bge-m3+deepseek-v2-16b+离线语音能力实现离线文档向量化问答语音版

发布于:2025-04-06 ⋅ 阅读:(19) ⋅ 点赞:(0)

ollama run deepseek-v2:16b
ollama pull bge-m3
1、离线听写效果的大幅度提升。50M+  1.3G(每次初始化都会很慢)---优化到首次初始化+使用0延迟响应。
2、文档问答历史问题处理与优化,文档问答离线策略讨论与参数暴露。
3、离线大模型答复中断处理策略,离线大模型的二次设置与修正。
4、打断模型答复与合成的实现策略。
5、服务的对外提供与生成安装包。
 

package com.day.util;

import org.apache.pdfbox.pdmodel.PDDocument;
import org.apache.pdfbox.text.PDFTextStripper;
import org.apache.poi.hwpf.HWPFDocument;
import org.apache.poi.hwpf.extractor.WordExtractor;
import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
import org.apache.poi.xwpf.usermodel.XWPFDocument;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;

public class ReadFiles {

    public static List<String> readDirectoryFiles(String directoryPath) throws IOException {
        List<String> resultList = new ArrayList<>();

        try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(directoryPath))) {
            for (Path path : stream) {
                if (Files.isRegularFile(path)) {
                    String fileName = path.getFileName().toString();
                    String fileContent = "";

                    try {
                        if (fileName.toLowerCase().endsWith(".txt")) {
                            fileContent = readTextFile(path);
                        } else if (fileName.toLowerCase().endsWith(".docx")) {
                            fileContent = readDocxFile(path);
                        } else if (fileName.toLowerCase().endsWith(".doc")) {
                            fileContent = readDocFile(path);
                        } else if (fileName.toLowerCase().endsWith(".pdf")) {
                            fileContent = readPdfFile(path);
                        }

                        if (!fileContent.isEmpty()) {
                            resultList.add("File: " + fileName + "\nContent:\n" + fileContent + "\n");
                        }
                    } catch (Exception e) {
                        System.err.println("Error reading file: " + fileName);
                        e.printStackTrace();
                    }
                }
            }
        }
        return resultList;
    }

    private static String readTextFile(Path path) throws IOException {
        return new String(Files.readAllBytes(path), StandardCharsets.UTF_8);
    }

    private static String readDocxFile(Path path) throws IOException {
        try (InputStream is = Files.newInputStream(path); XWPFDocument doc = new XWPFDocument(is); XWPFWordExtractor extractor = new XWPFWordExtractor(doc)) {
            return extractor.getText();
        }
    }

    private static String readDocFile(Path path) throws IOException {
        try (InputStream is = Files.newInputStream(path); HWPFDocument doc = new HWPFDocument(is); WordExtractor extractor = new WordExtractor(doc)) {
            return extractor.getText();
        }
    }

    private static String readPdfFile(Path path) throws IOException {
        try (PDDocument document = PDDocument.load(path.toFile())) {
            PDFTextStripper stripper = new PDFTextStripper();
            return stripper.getText(document);
        }
    }

   /* public static void main(String[] args) {
        try {
            List<String> contents = readDirectoryFiles("src/main/resources/");
            for (String content : contents) {
                System.out.println(content);
                System.out.println("-----------------------------");
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }*/

    public static List<String> readFilesWork(String dir) {
        try {
            return readDirectoryFiles(dir);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return new ArrayList<>();
    }
}
package com.day.util;

import java.util.ArrayList;
import java.util.List;

public class SubString {

    public static List<String> splitBySentenceWithinLimit(String input, Integer maxNumber) {
        List<String> result = new ArrayList<>();
        if (input == null || input.isEmpty()) {
            return result;
        }

        int currentPosition = 0;
        int totalLength = input.length();

        while (currentPosition < totalLength) {
            int endPosition = Math.min(currentPosition + maxNumber, totalLength);

            // 截取当前最大可能区间
            String currentChunk = input.substring(currentPosition, endPosition);

            // 查找最后一个句号的位置
            int lastDotIndex = currentChunk.lastIndexOf('。');

            if (lastDotIndex != -1) {  // 找到句号的情况
                int actualEnd = currentPosition + lastDotIndex + 1;
                result.add(input.substring(currentPosition, actualEnd));
                currentPosition = actualEnd;
            } else {  // 没有句号的情况
                result.add(currentChunk);
                currentPosition = endPosition;
            }
        }

        return result;
    }

/*    public static void main(String[] args) {
        // 测试示例
        String testStr = "作者:小飞飞,撰写于6月31日。\n" + "想当年,他所带领的军队以锐不可挡之势,横扫大江南北,可以说是在父兄基业上既往开来,成就了一番伟业。原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由然而生。威望的增加、权利的扩张丝毫没有改变他原有的样样子,他迈步走进岳楼,回忆起在湖北省张家界市的一段往事。那是一个薄雾蒙蒙的清晨,在急促行军途中他与一位素未谋面的人相逢,虽然之后并没有太多故事,却至今难以忘却,正当他的思绪陷入过往,忽然一阵震天的马蹄声夹杂着士兵的喧闹传来,报:“敌人来袭,我方战线危机,望将军火速驰援”。由于刚刚陷入过往的原因,他稍微愣了愣神,咆哮道:“大军听令,即刻出发”!军令如山。成群的士兵迅速从营房中跑出在校场上整齐队列,方阵如虹、战马昂首、刀枪如林、战旗迎风飘扬,将士身上的盔甲在阳光照射下,闪耀着金属的光泽。看着这支曾跟着他南征北战的队伍,他默默翻身登上战马,走在队伍最前面。营房外的道路两旁站满了欢送的百姓,大家希望将军能带领着军队,再次创造奇迹。作者:小飞飞,撰写于6月31日。\n" + "\n" + "想当年,他所带领的军队以锐不可挡之势,横扫大江南北,可以说是在父兄基业上既往开来,成就了一番伟业。原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由然而生。威望的增加、权利的扩张丝毫没有改变他原有的样样子,他迈步走进岳楼,回忆起在湖北省张家界市的一段往事。那是一个薄雾蒙蒙的清晨,在急促行军途中他与一位素未谋面的人相逢,虽然之后并没有太多故事,却至今难以忘却,正当他的思绪陷入过往,忽然一阵震天的马蹄声夹杂着士兵的喧闹传来,报:“敌人来袭,我方战线危机,望将军火速驰援”。由于刚刚陷入过往的原因,他稍微愣了愣神,咆哮道:“大军听令,即刻出发”!军令如山。成群的士兵迅速从营房中跑出在校场上整齐队列,方阵如虹、战马昂首、刀枪如林、战旗迎风飘扬,将士身上的盔甲在阳光照射下,闪耀着金属的光泽。看着这支曾跟着他南征北战的队伍,他默默翻身登上战马,走在队伍最前面。营房外的道路两旁站满了欢送的百姓,大家希望将军能带领着军队,再次创造奇迹。作者:小飞飞,撰写于6月31日。\n" + "想当年,他所带领的军队以锐不可挡之势,横扫大江南北,可以说是在父兄基业上既往开来,成就了一番伟业。原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由然而生。威望的增加、权利的扩张丝毫没有改变他原有的样样子,他迈步走进岳楼,回忆起在湖北省张家界市的一段往事。那是一个薄雾蒙蒙的清晨,在急促行军途中他与一位素未谋面的人相逢,虽然之后并没有太多故事,却至今难以忘却,正当他的思绪陷入过往,忽然一阵震天的马蹄声夹杂着士兵的喧闹传来,报:“敌人来袭,我方战线危机,望将军火速驰援”。由于刚刚陷入过往的原因,他稍微愣了愣神,咆哮道:“大军听令,即刻出发”!军令如山。成群的士兵迅速从营房中跑出在校场上整齐队列,方阵如虹、战马昂首、刀枪如林、战旗迎风飘扬,将士身上的盔甲在阳光照射下,闪耀着金属的光泽。看着这支曾跟着他南征北战的队伍,他默默翻身登上战马,走在队伍最前面。营房外的道路两旁站满了欢送的百姓,大家希望将军能带领着军队,再次创造奇迹。作者:小飞飞,撰写于6月31日。\n" + "想当年,他所带领的军队以锐不可挡之势,横扫大江南北,可以说是在父兄基业上既往开来,成就了一番伟业。原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由然而生。威望的增加、权利的扩张丝毫没有改变他原有的样样子,他迈步走进岳楼,回忆起在湖北省张家界市的一段往事。那是一个薄雾蒙蒙的清晨,在急促行军途中他与一位素未谋面的人相逢,虽然之后并没有太多故事,却至今难以忘却,正当他的思绪陷入过往,忽然一阵震天的马蹄声夹杂着士兵的喧闹传来,报:“敌人来袭,我方战线危机,望将军火速驰援”。由于刚刚陷入过往的原因,他稍微愣了愣神,咆哮道:“大军听令,即刻出发”!军令如山。成群的士兵迅速从营房中跑出在校场上整齐队列,方阵如虹、战马昂首、刀枪如林、战旗迎风飘扬,将士身上的盔甲在阳光照射下,闪耀着金属的光泽。看着这支曾跟着他南征北战的队伍,他默默翻身登上战马,走在队伍最前面。营房外的道路两旁站满了欢送的百姓,大家希望将军能带领着军队,再次创造奇迹。作者:小飞飞,撰写于6月31日。\n" + "想当年,他所带领的军队以锐不可挡之势,横扫大江南北,可以说是在父兄基业上既往开来,成就了一番伟业。原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由然而生。威望的增加、权利的扩张丝毫没有改变他原有的样样子,他迈步走进岳楼,回忆起在湖北省张家界市的一段往事。那是一个薄雾蒙蒙的清晨,在急促行军途中他与一位素未谋面的人相逢,虽然之后并没有太多故事,却至今难以忘却,正当他的思绪陷入过往,忽然一阵震天的马蹄声夹杂着士兵的喧闹传来,报:“敌人来袭,我方战线危机,望将军火速驰援”。由于刚刚陷入过往的原因,他稍微愣了愣神,咆哮道:“大军听令,即刻出发”!军令如山。成群的士兵迅速从营房中跑出在校场上整齐队列,方阵如虹、战马昂首、刀枪如林、战旗迎风飘扬,将士身上的盔甲在阳光照射下,闪耀着金属的光泽。看着这支曾跟着他南征北战的队伍,他默默翻身登上战马,走在队伍最前面。营房外的道路两旁站满了欢送的百姓,大家希望将军能带领着军队,再次创造奇迹。作者:小飞飞,撰写于6月31日。\n" + "想当年,他所带领的军队以锐不可挡之势,横扫大江南北,可以说是在父兄基业上既往开来,成就了一番伟业。原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由然而生。威望的增加、权利的扩张丝毫没有改变他原有的样样子,他迈步走进岳楼,回忆起在湖北省张家界市的一段往事。那是一个薄雾蒙蒙的清晨,在急促行军途中他与一位素未谋面的人相逢,虽然之后并没有太多故事,却至今难以忘却,正当他的思绪陷入过往,忽然一阵震天的马蹄声夹杂着士兵的喧闹传来,报:“敌人来袭,我方战线危机,望将军火速驰援”。由于刚刚陷入过往的原因,他稍微愣了愣神,咆哮道:“大军听令,即刻出发”!军令如山。成群的士兵迅速从营房中跑出在校场上整齐队列,方阵如虹、战马昂首、刀枪如林、战旗迎风飘扬,将士身上的盔甲在阳光照射下,闪耀着金属的光泽。看着这支曾跟着他南征北战的队伍,他默默翻身登上战马,走在队伍最前面。营房外的道路两旁站满了欢送的百姓,大家希望将军能带领着军队,再次创造奇迹。作者:小飞飞,撰写于6月31日。\n" + "想当年,他所带领的军队以锐不可挡之势,横扫大江南北,可以说是在父兄基业上既往开来,成就了一番伟业。原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由然而生。威望的增加、权利的扩张丝毫没有改变他原有的样样子,他迈步走进岳楼,回忆起在湖北省张家界市的一段往事。那是一个薄雾蒙蒙的清晨,在急促行军途中他与一位素未谋面的人相逢,虽然之后并没有太多故事,却至今难以忘却,正当他的思绪陷入过往,忽然一阵震天的马蹄声夹杂着士兵的喧闹传来,报:“敌人来袭,我方战线危机,望将军火速驰援”。由于刚刚陷入过往的原因,他稍微愣了愣神,咆哮道:“大军听令,即刻出发”!军令如山。成群的士兵迅速从营房中跑出在校场上整齐队列,方阵如虹、战马昂首、刀枪如林、战旗迎风飘扬,将士身上的盔甲在阳光照射下,闪耀着金属的光泽。看着这支曾跟着他南征北战的队伍,他默默翻身登上战马,走在队伍最前面。营房外的道路两旁站满了欢送的百姓,大家希望将军能带领着军队,再次创造奇迹。作者:小飞飞,撰写于6月31日。\n" + "\n" + "想当年,他所带领的军队以锐不可挡之势,横扫大江南北,可以说是在父兄基业上既往开来,成就了一番伟业。原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由然而生。威望的增加、权利的扩张丝毫没有改变他原有的样样子,他迈步走进岳楼,回忆起在湖北省张家界市的一段往事。那是一个薄雾蒙蒙的清晨,在急促行军途中他与一位素未谋面的人相逢,虽然之后并没有太多故事,却至今难以忘却,正当他的思绪陷入过往,忽然一阵震天的马蹄声夹杂着士兵的喧闹传来,报:“敌人来袭,我方战线危机,望将军火速驰援”。由于刚刚陷入过往的原因,他稍微愣了愣神,咆哮道:“大军听令,即刻出发”!军令如山。成群的士兵迅速从营房中跑出在校场上整齐队列,方阵如虹、战马昂首、刀枪如林、战旗迎风飘扬,将士身上的盔甲在阳光照射下,闪耀着金属的光泽。看着这支曾跟着他南征北战的队伍,他默默翻身登上战马,走在队伍最前面。营房外的道路两旁站满了欢送的百姓,大家希望将军能带领着军队,再次创造奇迹。作者:小飞飞,撰写于6月31日。\n" + "想当年,他所带领的军队以锐不可挡之势,横扫大江南北,可以说是在父兄基业上既往开来,成就了一番伟业。原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由然而生。威望的增加、权利的扩张丝毫没有改变他原有的样样子,他迈步走进岳楼,回忆起在湖北省张家界市的一段往事。那是一个薄雾蒙蒙的清晨,在急促行军途中他与一位素未谋面的人相逢,虽然之后并没有太多故事,却至今难以忘却,正当他的思绪陷入过往,忽然一阵震天的马蹄声夹杂着士兵的喧闹传来,报:“敌人来袭,我方战线危机,望将军火速驰援”。由于刚刚陷入过往的原因,他稍微愣了愣神,咆哮道:“大军听令,即刻出发”!军令如山。成群的士兵迅速从营房中跑出在校场上整齐队列,方阵如虹、战马昂首、刀枪如林、战旗迎风飘扬,将士身上的盔甲在阳光照射下,闪耀着金属的光泽。看着这支曾跟着他南征北战的队伍,他默默翻身登上战马,走在队伍最前面。营房外的道路两旁站满了欢送的百姓,大家希望将军能带领着军队,再次创造奇迹。作者:小飞飞,撰写于6月31日。\n" + "想当年,他所带领的军队以锐不可挡之势,横扫大江南北,可以说是在父兄基业上既往开来,成就了一番伟业。原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由然而生。威望的增加、权利的扩张丝毫没有改变他原有的样样子,他迈步走进岳楼,回忆起在湖北省张家界市的一段往事。那是一个薄雾蒙蒙的清晨,在急促行军途中他与一位素未谋面的人相逢,虽然之后并没有太多故事,却至今难以忘却,正当他的思绪陷入过往,忽然一阵震天的马蹄声夹杂着士兵的喧闹传来,报:“敌人来袭,我方战线危机,望将军火速驰援”。由于刚刚陷入过往的原因,他稍微愣了愣神,咆哮道:“大军听令,即刻出发”!军令如山。成群的士兵迅速从营房中跑出在校场上整齐队列,方阵如虹、战马昂首、刀枪如林、战旗迎风飘扬,将士身上的盔甲在阳光照射下,闪耀着金属的光泽。看着这支曾跟着他南征北战的队伍,他默默翻身登上战马,走在队伍最前面。营房外的道路两旁站满了欢送的百姓,大家希望将军能带领着军队,再次创造奇迹。作者:小飞飞,撰写于6月31日。\n" + "想当年,他所带领的军队以锐不可挡之势,横扫大江南北,可以说是在父兄基业上既往开来,成就了一番伟业。原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由然而生。威望的增加、权利的扩张丝毫没有改变他原有的样样子,他迈步走进岳楼,回忆起在湖北省张家界市的一段往事。那是一个薄雾蒙蒙的清晨,在急促行军途中他与一位素未谋面的人相逢,虽然之后并没有太多故事,却至今难以忘却,正当他的思绪陷入过往,忽然一阵震天的马蹄声夹杂着士兵的喧闹传来,报:“敌人来袭,我方战线危机,望将军火速驰援”。由于刚刚陷入过往的原因,他稍微愣了愣神,咆哮道:“大军听令,即刻出发”!军令如山。成群的士兵迅速从营房中跑出在校场上整齐队列,方阵如虹、战马昂首、刀枪如林、战旗迎风飘扬,将士身上的盔甲在阳光照射下,闪耀着金属的光泽。看着这支曾跟着他南征北战的队伍,他默默翻身登上战马,走在队伍最前面。营房外的道路两旁站满了欢送的百姓,大家希望将军能带领着军队,再次创造奇迹。作者:小飞飞,撰写于6月31日。\n" + "想当年,他所带领的军队以锐不可挡之势,横扫大江南北,可以说是在父兄基业上既往开来,成就了一番伟业。原本偏安一隅的小国,从他的手中变成了十三个州,国人对这位领袖的敬意由然而生。威望的增加、权利的扩张丝毫没有改变他原有的样样子,他迈步走进岳楼,回忆起在湖北省张家界市的一段往事。那是一个薄雾蒙蒙的清晨,在急促行军途中他与一位素未谋面的人相逢,虽然之后并没有太多故事,却至今难以忘却,正当他的思绪陷入过往,忽然一阵震天的马蹄声夹杂着士兵的喧闹传来,报:“敌人来袭,我方战线危机,望将军火速驰援”。由于刚刚陷入过往的原因,他稍微愣了愣神,咆哮道:“大军听令,即刻出发”!军令如山。成群的士兵迅速从营房中跑出在校场上整齐队列,方阵如虹、战马昂首、刀枪如林、战旗迎风飘扬,将士身上的盔甲在阳光照射下,闪耀着金属的光泽。看着这支曾跟着他南征北战的队伍,他默默翻身登上战马,走在队伍最前面。营房外的道路两旁站满了欢送的百姓,大家希望将军能带领着军队,再次创造奇迹。\u200B";  // 构造700字符的字符串
        List<String> splitResult = splitBySentenceWithinLimit(testStr, 500);

        System.out.println("分片数量:" + splitResult.size());
        for (int i = 0; i < splitResult.size(); i++) {
            System.out.printf("分片%d(长度%d):%s%n", i + 1, splitResult.get(i).length(), splitResult.get(i));
        }
    }*/

    public static List<String> subStringWork(String inputContent, Integer maxNumber) {
        // 测试示例
        List<String> splitResult = splitBySentenceWithinLimit(inputContent, maxNumber);

        System.out.println("分片数量:" + splitResult.size());
        /*for (int i = 0; i < splitResult.size(); i++) {
            System.out.printf("分片%d(长度%d):%s%n", i + 1, splitResult.get(i).length(), splitResult.get(i));
        }*/
        return splitResult;
    }
}


class Document {
    private String id;
    private String content;
    private List<Double> embedding;

    public Document(String id, String content) {
        this.id = id;
        this.content = content;
    }

    // Getters and setters
    public String getId() {
        return id;
    }

    public String getContent() {
        return content;
    }

    public List<Double> getEmbedding() {
        return embedding;
    }

    public void setEmbedding(List<Double> embedding) {
        this.embedding = embedding;
    }
}

interface EmbeddingFunction {
    List<Double> generateEmbedding(String text) throws Exception;
}

class OllamaEmbeddingFunction implements EmbeddingFunction {
    private final String model;
    private final String url;
    private final OkHttpClient client = new OkHttpClient();
    private final ObjectMapper mapper = new ObjectMapper();

    public OllamaEmbeddingFunction(String model, String url) {
        this.model = model;
        this.url = url;
    }

    @Override
    public List<Double> generateEmbedding(String text) throws Exception {
        // 构建请求体
        Map<String, Object> requestBody = new HashMap<>();
        requestBody.put("model", model);
        requestBody.put("prompt", text);

        RequestBody body = RequestBody.create(mapper.writeValueAsString(requestBody), MediaType.parse("application/json"));

        Request request = new Request.Builder().url(url).post(body).build();

        try (Response response = client.newCall(request).execute()) {
            if (!response.isSuccessful()) {
                throw new IOException("请求失败: " + response);
            }

            // 解析响应
            JsonNode root = mapper.readTree(response.body().bytes());
            JsonNode embeddingNode = root.get("embedding");

            List<Double> embedding = new ArrayList<>();
            for (JsonNode node : embeddingNode) {
                embedding.add(node.asDouble());
            }
            return embedding;
        }
    }
}

class Collection {
    private final String name;
    private final EmbeddingFunction embeddingFunction;
    private final List<Document> documentList = new ArrayList<>();

    public Collection(String name, EmbeddingFunction embeddingFunction) {
        this.name = name;
        this.embeddingFunction = embeddingFunction;
    }

    public void addDocuments(List<Document> documents) {
        int numCores = Runtime.getRuntime().availableProcessors();
        ExecutorService executor = Executors.newFixedThreadPool(numCores);

        List<Future<?>> futures = new ArrayList<>();
        for (Document doc : documents) {
            futures.add(executor.submit(() -> {
                try {
                    List<Double> embedding = embeddingFunction.generateEmbedding(doc.getContent());
                    synchronized (this.documentList) {
                        doc.setEmbedding(embedding);
                        this.documentList.add(doc);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }));
        }

        // 等待所有任务完成
        for (Future<?> future : futures) {
            try {
                future.get();
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }
        }
        executor.shutdown();
    }

    public Document getById(String id) {
        for (Document doc : documentList) {
            if (doc.getId().equals(id)) {
                return doc;
            }
        }
        return null;
    }

    public List<Result> query(String queryText, int topK) {
        try {
            List<Double> queryEmbedding = embeddingFunction.generateEmbedding(queryText);
            List<Result> results = new ArrayList<>();

            for (Document doc : documentList) {
                double similarity = cosineSimilarity(queryEmbedding, doc.getEmbedding());
                results.add(new Result(doc, similarity));
            }

            results.sort((a, b) -> Double.compare(b.getSimilarity(), a.getSimilarity()));
            return results.subList(0, Math.min(topK, results.size()));

        } catch (Exception e) {
            e.printStackTrace();
            return Collections.emptyList();
        }
    }

    private double cosineSimilarity(List<Double> vec1, List<Double> vec2) {
        double dotProduct = 0.0;
        double norm1 = 0.0;
        double norm2 = 0.0;

        for (int i = 0; i < vec1.size(); i++) {
            dotProduct += vec1.get(i) * vec2.get(i);
            norm1 += Math.pow(vec1.get(i), 2);
            norm2 += Math.pow(vec2.get(i), 2);
        }

        return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2));
    }
}

class Result {
    private final Document document;
    private final double similarity;

    public Result(Document document, double similarity) {
        this.document = document;
        this.similarity = similarity;
    }

    public Document getDocument() {
        return document;
    }

    public double getSimilarity() {
        return similarity;
    }
}