mirror of
https://github.com/trustgraph-ai/trustgraph.git
synced 2026-07-01 17:39:39 +02:00
init
This commit is contained in:
parent
c386f68743
commit
b6536eca38
100 changed files with 17680 additions and 377 deletions
80
ts/packages/flow/src/query/embeddings/qdrant-doc.ts
Normal file
80
ts/packages/flow/src/query/embeddings/qdrant-doc.ts
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
/**
|
||||
* Qdrant document embeddings query service.
|
||||
*
|
||||
* Input: vector, user, collection, limit
|
||||
* Output: list of { chunkId, score } matches
|
||||
*
|
||||
* Python reference: trustgraph-flow/trustgraph/query/doc_embeddings/qdrant/service.py
|
||||
*/
|
||||
|
||||
import { QdrantClient } from "@qdrant/js-client-rest";
|
||||
|
||||
export interface QdrantDocQueryConfig {
|
||||
url?: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export interface ChunkMatch {
|
||||
chunkId: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export interface DocEmbeddingsQueryRequest {
|
||||
vector: number[];
|
||||
user: string;
|
||||
collection: string;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export class QdrantDocEmbeddingsQuery {
|
||||
private client: QdrantClient;
|
||||
|
||||
constructor(config: QdrantDocQueryConfig = {}) {
|
||||
const url = config.url ?? process.env.QDRANT_URL ?? "http://localhost:6333";
|
||||
const apiKey = config.apiKey ?? process.env.QDRANT_API_KEY;
|
||||
|
||||
this.client = new QdrantClient({ url, apiKey });
|
||||
|
||||
console.log("[QdrantDocQuery] Query service initialized");
|
||||
}
|
||||
|
||||
async query(request: DocEmbeddingsQueryRequest): Promise<ChunkMatch[]> {
|
||||
const { vector, user, collection, limit } = request;
|
||||
|
||||
if (!vector || vector.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dim = vector.length;
|
||||
const collectionName = `d_${user}_${collection}_${dim}`;
|
||||
|
||||
// Check if collection exists -- return empty if not
|
||||
const exists = await this.client.collectionExists(collectionName);
|
||||
if (!exists.exists) {
|
||||
console.log(
|
||||
`[QdrantDocQuery] Collection ${collectionName} does not exist, returning empty results`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const searchResult = await this.client.search(collectionName, {
|
||||
vector,
|
||||
limit,
|
||||
with_payload: true,
|
||||
});
|
||||
|
||||
const chunks: ChunkMatch[] = [];
|
||||
for (const point of searchResult) {
|
||||
const payload = point.payload as Record<string, unknown> | undefined;
|
||||
const chunkId = payload?.chunk_id as string | undefined;
|
||||
if (chunkId) {
|
||||
chunks.push({
|
||||
chunkId,
|
||||
score: point.score,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return chunks;
|
||||
}
|
||||
}
|
||||
103
ts/packages/flow/src/query/embeddings/qdrant-graph.ts
Normal file
103
ts/packages/flow/src/query/embeddings/qdrant-graph.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
/**
|
||||
* Qdrant graph embeddings query service.
|
||||
*
|
||||
* Input: vector, user, collection, limit
|
||||
* Output: list of Term entities with scores, deduplicated by entity value
|
||||
*
|
||||
* Queries limit*2 points and deduplicates by entity value to ensure
|
||||
* we return up to `limit` unique entities.
|
||||
*
|
||||
* Python reference: trustgraph-flow/trustgraph/query/graph_embeddings/qdrant/service.py
|
||||
*/
|
||||
|
||||
import { QdrantClient } from "@qdrant/js-client-rest";
|
||||
import type { Term } from "@trustgraph/base";
|
||||
|
||||
export interface QdrantGraphQueryConfig {
|
||||
url?: string;
|
||||
apiKey?: string;
|
||||
}
|
||||
|
||||
export interface EntityMatch {
|
||||
entity: Term;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export interface GraphEmbeddingsQueryRequest {
|
||||
vector: number[];
|
||||
user: string;
|
||||
collection: string;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
function createTerm(value: string): Term {
|
||||
if (value.startsWith("http://") || value.startsWith("https://")) {
|
||||
return { type: "IRI", iri: value };
|
||||
}
|
||||
return { type: "LITERAL", value };
|
||||
}
|
||||
|
||||
export class QdrantGraphEmbeddingsQuery {
|
||||
private client: QdrantClient;
|
||||
|
||||
constructor(config: QdrantGraphQueryConfig = {}) {
|
||||
const url = config.url ?? process.env.QDRANT_URL ?? "http://localhost:6333";
|
||||
const apiKey = config.apiKey ?? process.env.QDRANT_API_KEY;
|
||||
|
||||
this.client = new QdrantClient({ url, apiKey });
|
||||
|
||||
console.log("[QdrantGraphQuery] Query service initialized");
|
||||
}
|
||||
|
||||
async query(request: GraphEmbeddingsQueryRequest): Promise<EntityMatch[]> {
|
||||
const { vector, user, collection, limit } = request;
|
||||
|
||||
if (!vector || vector.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const dim = vector.length;
|
||||
const collectionName = `t_${user}_${collection}_${dim}`;
|
||||
|
||||
// Check if collection exists -- return empty if not
|
||||
const exists = await this.client.collectionExists(collectionName);
|
||||
if (!exists.exists) {
|
||||
console.log(
|
||||
`[QdrantGraphQuery] Collection ${collectionName} does not exist, returning empty results`,
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Query 2x the limit so we have a better chance of getting `limit`
|
||||
// unique entities after deduplication (same heuristic as Python impl)
|
||||
const searchResult = await this.client.search(collectionName, {
|
||||
vector,
|
||||
limit: limit * 2,
|
||||
with_payload: true,
|
||||
});
|
||||
|
||||
const entitySet = new Set<string>();
|
||||
const entities: EntityMatch[] = [];
|
||||
|
||||
for (const point of searchResult) {
|
||||
const payload = point.payload as Record<string, unknown> | undefined;
|
||||
const entityValue = payload?.entity as string | undefined;
|
||||
if (!entityValue) continue;
|
||||
|
||||
// Deduplicate by entity value, keeping the highest score (results are
|
||||
// already sorted by score descending from Qdrant)
|
||||
if (!entitySet.has(entityValue)) {
|
||||
entitySet.add(entityValue);
|
||||
entities.push({
|
||||
entity: createTerm(entityValue),
|
||||
score: point.score,
|
||||
});
|
||||
}
|
||||
|
||||
// Stop once we have enough unique entities
|
||||
if (entities.length >= limit) break;
|
||||
}
|
||||
|
||||
return entities;
|
||||
}
|
||||
}
|
||||
243
ts/packages/flow/src/query/triples/falkordb.ts
Normal file
243
ts/packages/flow/src/query/triples/falkordb.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
/**
|
||||
* FalkorDB triples query service — queries RDF triples from FalkorDB.
|
||||
*
|
||||
* Implements all SPO query patterns (S, P, O, SP, SO, PO, SPO, *).
|
||||
*
|
||||
* Python reference: trustgraph-flow/trustgraph/query/triples/falkordb/service.py
|
||||
*/
|
||||
|
||||
import { createClient, Graph } from "falkordb";
|
||||
import type { Term, Triple } from "@trustgraph/base";
|
||||
|
||||
export interface FalkorDBQueryConfig {
|
||||
url?: string;
|
||||
database?: string;
|
||||
}
|
||||
|
||||
function termToValue(term: Term | undefined): string | null {
|
||||
if (!term) return null;
|
||||
switch (term.type) {
|
||||
case "IRI": return term.iri;
|
||||
case "LITERAL": return term.value;
|
||||
case "BLANK": return term.id;
|
||||
default: return null;
|
||||
}
|
||||
}
|
||||
|
||||
function createTerm(value: string): Term {
|
||||
if (value.startsWith("http://") || value.startsWith("https://")) {
|
||||
return { type: "IRI", iri: value };
|
||||
}
|
||||
return { type: "LITERAL", value };
|
||||
}
|
||||
|
||||
export class FalkorDBTriplesQuery {
|
||||
private graph: Graph;
|
||||
|
||||
constructor(config: FalkorDBQueryConfig = {}) {
|
||||
const url = config.url ?? process.env.FALKORDB_URL ?? "redis://localhost:6379";
|
||||
const database = config.database ?? "falkordb";
|
||||
|
||||
const client = createClient({ url });
|
||||
this.graph = new Graph(client, database);
|
||||
}
|
||||
|
||||
async queryTriples(
|
||||
s?: Term,
|
||||
p?: Term,
|
||||
o?: Term,
|
||||
limit = 100,
|
||||
): Promise<Triple[]> {
|
||||
const sv = termToValue(s);
|
||||
const pv = termToValue(p);
|
||||
const ov = termToValue(o);
|
||||
|
||||
const rawTriples: [string, string, string][] = [];
|
||||
|
||||
// Query both Node and Literal targets for each pattern
|
||||
if (sv && pv && ov) {
|
||||
// SPO — exact match
|
||||
await this.matchPattern(rawTriples, sv, pv, ov, limit);
|
||||
} else if (sv && pv) {
|
||||
// SP — known subject + predicate
|
||||
await this.matchSP(rawTriples, sv, pv, limit);
|
||||
} else if (sv && ov) {
|
||||
// SO — known subject + object
|
||||
await this.matchSO(rawTriples, sv, ov, limit);
|
||||
} else if (pv && ov) {
|
||||
// PO — known predicate + object
|
||||
await this.matchPO(rawTriples, pv, ov, limit);
|
||||
} else if (sv) {
|
||||
// S only
|
||||
await this.matchS(rawTriples, sv, limit);
|
||||
} else if (pv) {
|
||||
// P only
|
||||
await this.matchP(rawTriples, pv, limit);
|
||||
} else if (ov) {
|
||||
// O only
|
||||
await this.matchO(rawTriples, ov, limit);
|
||||
} else {
|
||||
// Wildcard — all triples
|
||||
await this.matchAll(rawTriples, limit);
|
||||
}
|
||||
|
||||
return rawTriples.slice(0, limit).map(([s, p, o]) => ({
|
||||
s: createTerm(s),
|
||||
p: createTerm(p),
|
||||
o: createTerm(o),
|
||||
}));
|
||||
}
|
||||
|
||||
private async matchPattern(
|
||||
out: [string, string, string][],
|
||||
sv: string, pv: string, ov: string, limit: number,
|
||||
): Promise<void> {
|
||||
for (const destType of ["Literal", "Node"] as const) {
|
||||
const destKey = destType === "Literal" ? "value" : "uri";
|
||||
const result = await this.graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel {uri: $rel}]->(dest:${destType} {${destKey}: $dest}) ` +
|
||||
`RETURN src.uri LIMIT ${limit}`,
|
||||
{ params: { src: sv, rel: pv, dest: ov } },
|
||||
);
|
||||
for (const _rec of (result.data ?? []) as unknown[][]) {
|
||||
out.push([sv, pv, ov]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async matchSP(
|
||||
out: [string, string, string][],
|
||||
sv: string, pv: string, limit: number,
|
||||
): Promise<void> {
|
||||
// Literals
|
||||
const litResult = await this.graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel {uri: $rel}]->(dest:Literal) ` +
|
||||
`RETURN dest.value as dest LIMIT ${limit}`,
|
||||
{ params: { src: sv, rel: pv } },
|
||||
);
|
||||
for (const rec of (litResult.data ?? []) as string[][]) {
|
||||
out.push([sv, pv, rec[0] as string]);
|
||||
}
|
||||
// Nodes
|
||||
const nodeResult = await this.graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel {uri: $rel}]->(dest:Node) ` +
|
||||
`RETURN dest.uri as dest LIMIT ${limit}`,
|
||||
{ params: { src: sv, rel: pv } },
|
||||
);
|
||||
for (const rec of (nodeResult.data ?? []) as string[][]) {
|
||||
out.push([sv, pv, rec[0] as string]);
|
||||
}
|
||||
}
|
||||
|
||||
private async matchSO(
|
||||
out: [string, string, string][],
|
||||
sv: string, ov: string, limit: number,
|
||||
): Promise<void> {
|
||||
for (const [destType, destKey] of [["Literal", "value"], ["Node", "uri"]] as const) {
|
||||
const result = await this.graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel]->(dest:${destType} {${destKey}: $dest}) ` +
|
||||
`RETURN rel.uri as rel LIMIT ${limit}`,
|
||||
{ params: { src: sv, dest: ov } },
|
||||
);
|
||||
for (const rec of (result.data ?? []) as string[][]) {
|
||||
out.push([sv, rec[0] as string, ov]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async matchPO(
|
||||
out: [string, string, string][],
|
||||
pv: string, ov: string, limit: number,
|
||||
): Promise<void> {
|
||||
for (const [destType, destKey] of [["Literal", "value"], ["Node", "uri"]] as const) {
|
||||
const result = await this.graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel {uri: $rel}]->(dest:${destType} {${destKey}: $dest}) ` +
|
||||
`RETURN src.uri as src LIMIT ${limit}`,
|
||||
{ params: { rel: pv, dest: ov } },
|
||||
);
|
||||
for (const rec of (result.data ?? []) as string[][]) {
|
||||
out.push([rec[0] as string, pv, ov]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async matchS(
|
||||
out: [string, string, string][],
|
||||
sv: string, limit: number,
|
||||
): Promise<void> {
|
||||
const litResult = await this.graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel]->(dest:Literal) ` +
|
||||
`RETURN rel.uri as rel, dest.value as dest LIMIT ${limit}`,
|
||||
{ params: { src: sv } },
|
||||
);
|
||||
for (const rec of (litResult.data ?? []) as string[][]) {
|
||||
out.push([sv, rec[0] as string, rec[1] as string]);
|
||||
}
|
||||
const nodeResult = await this.graph.query(
|
||||
`MATCH (src:Node {uri: $src})-[rel:Rel]->(dest:Node) ` +
|
||||
`RETURN rel.uri as rel, dest.uri as dest LIMIT ${limit}`,
|
||||
{ params: { src: sv } },
|
||||
);
|
||||
for (const rec of (nodeResult.data ?? []) as string[][]) {
|
||||
out.push([sv, rec[0] as string, rec[1] as string]);
|
||||
}
|
||||
}
|
||||
|
||||
private async matchP(
|
||||
out: [string, string, string][],
|
||||
pv: string, limit: number,
|
||||
): Promise<void> {
|
||||
const litResult = await this.graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel {uri: $rel}]->(dest:Literal) ` +
|
||||
`RETURN src.uri as src, dest.value as dest LIMIT ${limit}`,
|
||||
{ params: { rel: pv } },
|
||||
);
|
||||
for (const rec of (litResult.data ?? []) as string[][]) {
|
||||
out.push([rec[0] as string, pv, rec[1] as string]);
|
||||
}
|
||||
const nodeResult = await this.graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel {uri: $rel}]->(dest:Node) ` +
|
||||
`RETURN src.uri as src, dest.uri as dest LIMIT ${limit}`,
|
||||
{ params: { rel: pv } },
|
||||
);
|
||||
for (const rec of (nodeResult.data ?? []) as string[][]) {
|
||||
out.push([rec[0] as string, pv, rec[1] as string]);
|
||||
}
|
||||
}
|
||||
|
||||
private async matchO(
|
||||
out: [string, string, string][],
|
||||
ov: string, limit: number,
|
||||
): Promise<void> {
|
||||
for (const [destType, destKey] of [["Literal", "value"], ["Node", "uri"]] as const) {
|
||||
const result = await this.graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel]->(dest:${destType} {${destKey}: $dest}) ` +
|
||||
`RETURN src.uri as src, rel.uri as rel LIMIT ${limit}`,
|
||||
{ params: { dest: ov } },
|
||||
);
|
||||
for (const rec of (result.data ?? []) as string[][]) {
|
||||
out.push([rec[0] as string, rec[1] as string, ov]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async matchAll(
|
||||
out: [string, string, string][],
|
||||
limit: number,
|
||||
): Promise<void> {
|
||||
const litResult = await this.graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel]->(dest:Literal) ` +
|
||||
`RETURN src.uri as src, rel.uri as rel, dest.value as dest LIMIT ${limit}`,
|
||||
);
|
||||
for (const rec of (litResult.data ?? []) as string[][]) {
|
||||
out.push([rec[0] as string, rec[1] as string, rec[2] as string]);
|
||||
}
|
||||
const nodeResult = await this.graph.query(
|
||||
`MATCH (src:Node)-[rel:Rel]->(dest:Node) ` +
|
||||
`RETURN src.uri as src, rel.uri as rel, dest.uri as dest LIMIT ${limit}`,
|
||||
);
|
||||
for (const rec of (nodeResult.data ?? []) as string[][]) {
|
||||
out.push([rec[0] as string, rec[1] as string, rec[2] as string]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue