Skip to content

Commit 3996585

Browse files
committed
feat: initial code
1 parent 3edb9f0 commit 3996585

21 files changed

Lines changed: 728 additions & 0 deletions

.github/workflows/build.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: Build
2+
3+
on:
4+
push:
5+
branches: ['**']
6+
pull_request:
7+
branches: ['**']
8+
9+
jobs:
10+
build:
11+
runs-on: ubuntu-latest
12+
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Setup pnpm
17+
uses: pnpm/action-setup@v4
18+
with:
19+
version: 9
20+
21+
- name: Setup Node.js
22+
uses: actions/setup-node@v4
23+
with:
24+
node-version: '20'
25+
cache: 'pnpm'
26+
27+
- name: Install dependencies
28+
run: pnpm install
29+
30+
- name: Run build
31+
run: pnpm build
32+
33+
- name: Run tests
34+
run: pnpm test

.gitignore

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
node_modules/
2+
coverage/
3+
dist/
4+
5+
*.log
6+
.DS_Store
7+
.test-tmp
8+
.cache
9+
10+
package-lock.json
11+
yarn.lock
12+
pnpm-lock.yaml
13+
pnpm-workspace.yaml

package.json

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
{
2+
"name": "@antv/context",
3+
"version": "0.1.0",
4+
"description": "A local context retrieval library that enables semantic search over your documentation. It loads documents (Markdown, JSON, Text), vectorizes them using [Transformers.js](https://huggingface.co/transformers.js), and stores vectors locally in `.zvec` files for fast semantic querying. ",
5+
"main": "dist/index.js",
6+
"types": "dist/index.d.ts",
7+
"type": "module",
8+
"scripts": {
9+
"build": "tsc",
10+
"test": "vitest run --coverage"
11+
},
12+
"keywords": [
13+
"context",
14+
"semantic-search",
15+
"embedding",
16+
"vector",
17+
"transformers",
18+
"huggingface",
19+
"zvec",
20+
"antv"
21+
],
22+
"license": "MIT",
23+
"dependencies": {
24+
"@huggingface/transformers": "^4.2.0",
25+
"@zvec/zvec": "^0.5.0",
26+
"glob": "^13.0.6",
27+
"gray-matter": "^4.0.3"
28+
},
29+
"devDependencies": {
30+
"@types/node": "^20.0.0",
31+
"@vitest/coverage-v8": "^3.2.6",
32+
"typescript": "^5.0.0",
33+
"vitest": "^3.2.6"
34+
},
35+
"repository": "git@github.com:antvis/context.git"
36+
}

src/context.ts

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import * as path from 'path';
2+
import * as fs from 'fs';
3+
import { glob } from 'glob';
4+
import { ContextOptions, QueryOptions, QueryResult } from './types';
5+
import { TransformerVectorizer } from './vectorizer/transformer';
6+
import { ZVecStore } from './storage/zvec-store';
7+
import { Loader, MarkdownLoader, JsonLoader, TextLoader } from './loaders';
8+
9+
export class Context {
10+
private readonly vectorsDir: string;
11+
private readonly model: string;
12+
private readonly vectorizer: TransformerVectorizer;
13+
private readonly stores: Map<string, ZVecStore> = new Map();
14+
private readonly loaders: Loader[];
15+
16+
private constructor(options: ContextOptions) {
17+
this.vectorsDir = options.vectorsDir;
18+
this.model = options.model || 'sentence-transformers/all-MiniLM-L6-v2';
19+
this.vectorizer = new TransformerVectorizer(this.model);
20+
this.loaders = [
21+
new MarkdownLoader(),
22+
new JsonLoader(),
23+
new TextLoader(),
24+
];
25+
}
26+
27+
static async create(options: ContextOptions): Promise<Context> {
28+
const ctx = new Context(options);
29+
await ctx.vectorizer.initialize();
30+
31+
// Ensure vectors directory exists
32+
if (!fs.existsSync(options.vectorsDir)) {
33+
fs.mkdirSync(options.vectorsDir, { recursive: true });
34+
}
35+
36+
return ctx;
37+
}
38+
39+
private getLoader(filePath: string): Loader | undefined {
40+
return this.loaders.find((loader) => loader.canHandle(filePath));
41+
}
42+
43+
private getStoreFilePath(library: string): string {
44+
return path.join(this.vectorsDir, `${library}.zvec`);
45+
}
46+
47+
private async getOrCreateStore(library: string): Promise<ZVecStore> {
48+
if (this.stores.has(library)) {
49+
return this.stores.get(library)!;
50+
}
51+
52+
const filePath = this.getStoreFilePath(library);
53+
const store = await ZVecStore.create(filePath);
54+
55+
this.stores.set(library, store);
56+
return store;
57+
}
58+
59+
async load(library: string, pattern: string | string[]): Promise<void> {
60+
const patterns = Array.isArray(pattern) ? pattern : [pattern];
61+
const files = await glob(patterns, { absolute: true });
62+
const store = await this.getOrCreateStore(library);
63+
64+
for (const filePath of files) {
65+
const loader = this.getLoader(filePath);
66+
if (!loader) continue;
67+
68+
const doc = await loader.load(filePath);
69+
const vector = await this.vectorizer.embed(doc.content);
70+
store.add(doc.id, vector, doc.content, doc.meta);
71+
}
72+
73+
// Save to disk
74+
store.save();
75+
}
76+
77+
async query(text: string, options: QueryOptions): Promise<QueryResult[]> {
78+
const store = await this.getOrCreateStore(options.library);
79+
const vector = await this.vectorizer.embed(text);
80+
const searchResults = store.search(vector, options.topK || 5);
81+
82+
return searchResults.map((result) => {
83+
const doc = store.getDoc(result.id)!;
84+
return {
85+
id: result.id,
86+
content: doc.content,
87+
score: result.score,
88+
meta: doc.meta,
89+
};
90+
});
91+
}
92+
}

src/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export { Context } from './context';
2+
export * from './types';

src/loaders/base.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { Document } from '../types';
2+
3+
export interface Loader {
4+
canHandle(filePath: string): boolean;
5+
load(filePath: string): Promise<Document>;
6+
}

src/loaders/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export { Loader } from './base';
2+
export { MarkdownLoader } from './markdown';
3+
export { JsonLoader } from './json';
4+
export { TextLoader } from './text';

src/loaders/json.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import * as fs from 'fs/promises';
2+
import * as path from 'path';
3+
import { Loader } from './base';
4+
import { Document } from '../types';
5+
6+
export class JsonLoader implements Loader {
7+
canHandle(filePath: string): boolean {
8+
return filePath.endsWith('.json');
9+
}
10+
11+
async load(filePath: string): Promise<Document> {
12+
const content = await fs.readFile(filePath, 'utf-8');
13+
const data = JSON.parse(content);
14+
15+
return {
16+
id: path.basename(filePath),
17+
content: typeof data === 'string' ? data : JSON.stringify(data, null, 2),
18+
};
19+
}
20+
}

src/loaders/markdown.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import * as fs from 'fs/promises';
2+
import * as path from 'path';
3+
import matter from 'gray-matter';
4+
import { Loader } from './base';
5+
import { Document } from '../types';
6+
7+
export class MarkdownLoader implements Loader {
8+
canHandle(filePath: string): boolean {
9+
return filePath.endsWith('.md') || filePath.endsWith('.markdown');
10+
}
11+
12+
async load(filePath: string): Promise<Document> {
13+
const content = await fs.readFile(filePath, 'utf-8');
14+
const { data: meta, content: body } = matter(content);
15+
16+
// 使用文件名作为 ID,避免路径中的特殊字符
17+
const id = path.basename(filePath);
18+
19+
return {
20+
id,
21+
content: body.trim(),
22+
meta,
23+
};
24+
}
25+
}

src/loaders/text.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import * as fs from 'fs/promises';
2+
import * as path from 'path';
3+
import { Loader } from './base';
4+
import { Document } from '../types';
5+
6+
export class TextLoader implements Loader {
7+
canHandle(filePath: string): boolean {
8+
return filePath.endsWith('.txt');
9+
}
10+
11+
async load(filePath: string): Promise<Document> {
12+
const content = await fs.readFile(filePath, 'utf-8');
13+
14+
return {
15+
id: path.basename(filePath),
16+
content: content.trim(),
17+
};
18+
}
19+
}

0 commit comments

Comments
 (0)