Ctrl K
ComunidadGuías…

examples/text-hybrid-search.mjs

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env node
// Example: hybrid search: BM25 and vector search, fused by reciprocal rank fusion.
//
// HybridSearcher asks a vector store and a BM25 index for candidates and fuses the two
// rankings. Docs: docs/text/LEXICAL_AND_HYBRID_SEARCH.md.
//
// This script demonstrates:
//   - a short handbook cut into passages by SemanticChunker
//   - each passage written to an InMemoryVectorStore and a BM25Index under one id
//   - HybridSearcher with 'rrf' (the default), 'weighted-sum' and 'interleave'
//   - each result's fused score and its rank in the vector and BM25 lists
//   - reciprocalRankFusion() over two ranked lists of your own
//
// The embedder below is a stand-in, so the script runs with no API key: it hashes each
// word of a text into one of 256 buckets, and its vectors match words, not meanings. For
// real embeddings, pass an object whose generateEmbeddings({ texts }) answers
// { embeddings } from your provider, for example one that calls embedText() from
// '@framers/agentos'.
//
// Usage (no API key):
//   pnpm install && pnpm run build
//   node examples/text-hybrid-search.mjs

import { lexicalTokens } from '@framers/agentos/cognition/library';
import { reciprocalRankFusion } from '@framers/agentos/cognition/memory';
import { BM25Index, HybridSearcher, InMemoryVectorStore, SemanticChunker } from '@framers/agentos/cognition/rag';

const DIMENSIONS = 256;

/** The stand-in embedder: each word adds 1 to the bucket its hash selects. */
function embed(text) {
  const vector = new Array(DIMENSIONS).fill(0);
  for (const word of lexicalTokens(text)) {
    let hash = 0;
    for (const char of word) hash = (hash * 31 + char.codePointAt(0)) >>> 0;
    vector[hash % DIMENSIONS] += 1;
  }
  return vector;
}

// HybridSearcher calls generateEmbeddings({ texts: [query] }) and reads embeddings[0].
const embeddings = {
  async generateEmbeddings({ texts }) {
    const list = Array.isArray(texts) ? texts : [texts];
    return { embeddings: list.map(embed), modelId: 'hashed-words', providerId: 'example', usage: { totalTokens: 0 } };
  },
};

const handbook = `# Refunds

A refund goes back to the card that paid. Refunds take five to ten business days to appear.

# Shipping

Orders ship within two business days. Tracking numbers arrive by email once the parcel leaves the warehouse.

# Accounts

Reset a password from the sign-in page. Passwords expire after ninety days.

# Security

Two-factor codes arrive by text message. Each code works once and expires after ten minutes.`;

// Each Markdown heading starts a chunk.
const passages = new SemanticChunker({ overlap: 0 }).chunk(handbook);
console.log(`\n${passages.length} passages: ${passages.map((passage) => passage.boundaryType).join(', ')}\n`);

const store = new InMemoryVectorStore();
await store.initialize({ id: 'handbook', type: 'in_memory' });
await store.createCollection('handbook', DIMENSIONS);

const documents = passages.map((passage) => ({ id: `handbook#${passage.index}`, text: passage.text.trim() }));
await store.upsert(
  'handbook',
  documents.map((doc) => ({ id: doc.id, embedding: embed(doc.text), textContent: doc.text })),
);
const bm25 = new BM25Index();
bm25.addDocuments(documents);

const query = 'when does my refund arrive';
console.log(`\nquery: ${query}`);

for (const fusionMethod of ['rrf', 'weighted-sum', 'interleave']) {
  const searcher = new HybridSearcher(store, embeddings, bm25, { fusionMethod });
  const results = await searcher.search(query, 'handbook', 3);
  console.log(`\n=== ${fusionMethod} ===\n`);
  for (const result of results) {
    // A result only BM25 found has no textContent: InMemoryVectorStore has no fetchByIds to fill it in.
    // 'weighted-sum' sets no ranks, and 'interleave' one rank for each result, from the list it came from.
    const heading = (result.textContent ?? '').split('\n')[0];
    console.log(
      `${result.id.padEnd(11)} score ${result.score.toFixed(4)}  vector rank ${result.denseRank ?? '-'}  ` +
        `BM25 rank ${result.sparseRank ?? '-'}  ${heading}`,
    );
  }
}

console.log('\n=== reciprocalRankFusion() over lists of your own ===\n');
console.log(
  reciprocalRankFusion(
    [{ id: 'a', rank: 1 }, { id: 'b', rank: 2 }],
    [{ id: 'b', rank: 1 }, { id: 'c', rank: 2 }],
  ),
);