向量数据库 
同样的,ChatLuna 也支持接入向量数据库。
注册插件 
所有需要接入功能到 ChatLuna 的插件,都得新建 ChatLunaPlugin 实例,并注册到 ChatLuna 服务中。
import { ChatLunaPlugin } from 'koishi-plugin-chatluna/services/chat'
import { Context, Schema } from 'koishi'
export function apply(ctx: Context, config: Config) {
    const plugin = new ChatLunaPlugin(ctx, config, 'your-plugin-name', false)
    ctx.on('ready', async () => {
        // 继续...
    })
}NOTE
如果你的插件不需要注册模型适配器,ChatLunaPlugin 的构造函数需要传入 false 作为第四个参数。 该参数默认为 true,表示插件需要注册模型适配器。
配置 Schema 
如果你的向量数据库需要连接 URL 等参数,则需要自行声明 Schema。
import { ChatLunaPlugin } from 'koishi-plugin-chatluna/services/chat'
import { Context, Schema } from 'koishi'
export interface Config extends ChatLunaPlugin.Config {
    milvusUrl: string
    milvusUsername: string
    milvusPassword: string
}
export const Config: Schema<Config> = Schema.intersect([
    Schema.object({
        milvusUrl: Schema.string()
            .role('url')
            .default('http://127.0.0.1:19530'),
        milvusUsername: Schema.string().default(''),
        milvusPassword: Schema.string().role('secret').default('')
    })
]) as any例如上面的 Schema 中,就声明了 Milvus 的连接 URL、用户名和密码。
实现 ChatLunaSaveableVectorStore 包装类 
在注册向量数据库之前,你需要创建一个继承自 ChatLunaSaveableVectorStore 的包装类。
这个包装类负责包装 LangChain 的向量存储,并提供 ChatLuna 所需的特定功能。
创建包装类 
以 Milvus 为例,创建一个包装类:
import {
    ChatLunaSaveableVectorDelete,
    ChatLunaSaveableVectorStore,
    ChatLunaSaveableVectorStoreInput
} from 'koishi-plugin-chatluna/llm-core/vectorstores'
import { Milvus } from '@langchain/community/vectorstores/milvus'
import crypto from 'crypto'
import { DocumentInterface } from '@langchain/core/documents'
export class MilvusVectorStore extends ChatLunaSaveableVectorStore<Milvus> {
    private _key: string
    private _createCollection: () => Promise<void>
    constructor(input: MilvusVectorStoreInput) {
        super(input)
        this._key = input.key
        this._createCollection = input.createCollection
    }
    addDocuments(
        documents: DocumentInterface[],
        options?: Parameters<Milvus['addDocuments']>[1]
    ): Promise<string[] | void> {
        let ids = options?.ids ?? []
        ids = documents.map((document, i) => {
            const id = ids[i] ?? document.id ?? crypto.randomUUID()
            document.id = id
            document.metadata = {
                source: 'unknown',
                ...document.metadata,
                raw_id: id
            }
            // Milvus 不支持 UUID 中的 '-' 字符,需要替换
            return id.replaceAll('-', '_')
        })
        return super.addDocuments(documents, {
            ...options,
            ids
        })
    }
    async delete(options: ChatLunaSaveableVectorDelete): Promise<void> {
        if (options.deleteAll) {
            // 删除整个集合和分区
            await this._store.client.releasePartitions({
                collection_name: 'chatluna_collection',
                partition_names: [this._key]
            })
            await this._store.client.releaseCollection({
                collection_name: 'chatluna_collection'
            })
            await this._store.client.dropPartition({
                collection_name: 'chatluna_collection',
                partition_name: this._key
            })
            await this._store.client.dropCollection({
                collection_name: 'chatluna_collection'
            })
            await super.delete(options)
            return
        }
        const ids: string[] = []
        if (options.ids) {
            ids.push(...options.ids.map((id) => id.replaceAll('-', '_')))
        }
        if (options.documents) {
            const documentIds = options.documents
                ?.map((document) => {
                    const id = document.metadata?.raw_id as string | undefined
                    return id != null ? id.replaceAll('-', '_') : undefined
                })
                .filter((id): id is string => id != null)
            ids.push(...documentIds)
        }
        if (ids.length > 0) {
            const deleteResp = await this._store.client.delete({
                collection_name: this._store.collectionName,
                partition_name: this._key,
                ids
            })
            if (deleteResp.status.error_code !== 'Success') {
                throw new Error(
                    `Error deleting data with ids: ${JSON.stringify(deleteResp)}`
                )
            }
        }
        await super.delete(options)
    }
    async similaritySearchVectorWithScore(
        query: number[],
        k: number,
        filter?: this['FilterType']
    ): Promise<[DocumentInterface, number][]> {
        // 检查集合是否存在,如果不存在则重新创建
        const hasColResp = await this._store.client.hasCollection({
            collection_name: this._store.collectionName
        })
        if (hasColResp.status.error_code !== 'Success') {
            throw new Error(`Error checking collection: ${hasColResp}`)
        }
        if (hasColResp.value === false) {
            console.warn(
                `Collection ${this._store.collectionName} does not exist, ensure all data and recreate collection.`
            )
            // 触发重新索引
            await this._createCollection()
        }
        return super.similaritySearchVectorWithScore(query, k, filter)
    }
    async save() {
        // Milvus 会自动保存数据,无需显式保存
    }
}
export interface MilvusVectorStoreInput
    extends ChatLunaSaveableVectorStoreInput<Milvus> {
    key: string
    createCollection: () => Promise<void>
}包装类需要实现以下关键方法:
addDocuments: 添加文档时,为每个文档生成唯一 ID 并存储在 metadata 中delete: 支持删除指定文档或清空整个数据库similaritySearchVectorWithScore: 最核心的相似度搜索save: 保存向量数据库的状态(某些数据库如 Milvus 会在添加时就自动保存)
WARNING
不同的向量数据库可能有不同的限制和要求。例如 Milvus 不支持 UUID 中的 - 字符,需要替换为 _。
注册向量数据库 
实现包装类后,就可以注册向量数据库了。
使用 ChatLunaPlugin 实例的 registerVectorStore 方法注册:
import { Milvus } from '@langchain/community/vectorstores/milvus'
import { MilvusVectorStore } from './base/milvus'
import { DataBaseDocstore } from 'koishi-plugin-chatluna/llm-core/vectorstores'
import { Document } from '@langchain/core/documents'
import { randomUUID } from 'crypto'
import {
    ChatLunaError,
    ChatLunaErrorCode
} from 'koishi-plugin-chatluna/utils/error'
plugin.registerVectorStore('milvus', async (params) => {
    const embeddings = params.embeddings
    const key = sanitizeMilvusName(params.key ?? 'chatluna')
    const databaseDocstore = new DataBaseDocstore(ctx, key)
    logger.debug(`Loading milvus store with partition: %c`, key)
    const testVector = await embeddings.embedQuery('test')
    if (testVector.length === 0) {
        throw new ChatLunaError(
            ChatLunaErrorCode.VECTOR_STORE_EMBEDDING_DIMENSION_MISMATCH,
            new Error(
                'Embedding dimension is 0. Try changing the embeddings model.'
            )
        )
    }
    const vectorStore = new Milvus(embeddings, {
        collectionName: 'chatluna_collection',
        partitionName: key,
        url: config.milvusUrl,
        autoId: false,
        username: config.milvusUsername,
        password: config.milvusPassword,
        textFieldMaxLength: 3000
    })
    const createCollection = async () => {
        // 清理旧集合
        await vectorStore.client.releasePartitions({
            collection_name: 'chatluna_collection',
            partition_names: [key]
        })
        await vectorStore.client.releaseCollection({
            collection_name: 'chatluna_collection'
        })
        await vectorStore.client.dropPartition({
            collection_name: 'chatluna_collection',
            partition_name: key
        })
        await vectorStore.client.dropCollection({
            collection_name: 'chatluna_collection'
        })
        // 创建新集合时使用测试文档确定字段类型
        let documents: Document[] = [
            new Document({
                pageContent: 'A',
                id: randomUUID(),
                metadata: {
                    raw_id: 'z'.repeat(100),
                    source: 'z'.repeat(100),
                    expirationDate: 'z'.repeat(100),
                    createdAt: 'z'.repeat(100),
                    updateAt: 'z'.repeat(100),
                    time: 'z'.repeat(100),
                    user: 'z'.repeat(100),
                    userId: 'z'.repeat(100),
                    type: 'z'.repeat(100),
                    importance: 0
                }
            })
        ]
        await vectorStore.ensureCollection([testVector], documents)
        await vectorStore.ensurePartition()
        // 从 docstore 恢复所有文档
        documents = await databaseDocstore.list()
        await vectorStore.addDocuments(documents)
    }
    try {
        const sampleDoc = new Document({
            pageContent: 'test',
            metadata: {
                raw_id: 'z'.repeat(100),
                source: 'z'.repeat(100),
                expirationDate: 'z'.repeat(100),
                createdAt: 'z'.repeat(100),
                updateAt: 'z'.repeat(100),
                time: 'z'.repeat(100),
                user: 'z'.repeat(100),
                userId: 'z'.repeat(100),
                type: 'z'.repeat(100),
                importance: 0
            }
        })
        await vectorStore.ensureCollection([testVector], [sampleDoc])
        await vectorStore.ensurePartition()
        await vectorStore.similaritySearchVectorWithScore(testVector, 1)
    } catch (e) {
        logger.warn(
            'Error occurred when initializing milvus collection. Will recreate collection.'
        )
        logger.debug(e)
        try {
            await createCollection()
        } catch (e) {
            logger.error(e)
            throw new ChatLunaError(
                ChatLunaErrorCode.VECTOR_STORE_INIT_ERROR,
                new Error('Failed to initialize Milvus collection')
            )
        }
    }
    const wrapperStore = new MilvusVectorStore({
        store: vectorStore,
        docstore: databaseDocstore,
        key,
        createCollection,
        embeddings
    })
    return wrapperStore
})
function sanitizeMilvusName(name: string) {
    let s = name.replace(/[^A-Za-z0-9_]/g, '_')
    if (!/^[A-Za-z]/.test(s)) s = `p_${s}`
    return s.slice(0, 255)
}注册向量数据库时,需要完成以下步骤:
- 获取嵌入模型和唯一标识: 从 
params中获取embeddings和key - 创建 
DataBaseDocstore: 用于持久化存储文档内容 - 初始化向量存储: 创建 LangChain 的向量存储实例
 - 定义重建索引函数: 
createCollection函数负责在集合损坏或嵌入维度变化时重建索引 - 初始化检查: 测试向量存储是否正常工作
 - 创建包装实例: 使用自定义的包装类包装向量存储
 
提示
DataBaseDocstore 会持久化存储所有文档的内容。当向量数据库集合损坏或嵌入模型维度发生变化时,可以从 DataBaseDocstore 恢复所有文档并重新索引。
重新索引 
你需要在实现的代码内手动检查以下的情况:
- 嵌入模型的维度发生变化
 - 向量数据库的配置发生变化
 
当检测到这些情况时,你需要手动调用之前实现的重建索引函数。
注意,重建索引时,你需要从 DataBaseDocstore 中读取所有已保存的文档,使用新的嵌入模型重新生成向量并存储。
资源参考 
请参考 ChatLuna 官方的向量数据库服务插件 chatluna-vector-store,获取更多实现样例。