brainy/dist/utils/metadataFilter.js
David Snelling f8c45f2d8d Initial commit: Brainy - Multi-Dimensional AI Database
Open source vector database with HNSW indexing, graph relationships,
and metadata facets. Features CLI with professional augmentation registry
integration for discovering extensions and capabilities.
2025-08-18 17:35:06 -07:00

229 lines
No EOL
7.5 KiB
JavaScript

/**
* Smart metadata filtering for vector search
* Filters DURING search to ensure relevant results
* Simple API that just works without configuration
*/
/**
* Check if a value matches a query with operators
*/
function matchesQuery(value, query) {
// Direct equality check
if (typeof query !== 'object' || query === null || Array.isArray(query)) {
return value === query;
}
// Check for MongoDB-style operators
for (const [op, operand] of Object.entries(query)) {
switch (op) {
case '$eq':
if (value !== operand)
return false;
break;
case '$ne':
if (value === operand)
return false;
break;
case '$gt':
if (typeof value !== 'number' || typeof operand !== 'number' || !(value > operand))
return false;
break;
case '$gte':
if (typeof value !== 'number' || typeof operand !== 'number' || !(value >= operand))
return false;
break;
case '$lt':
if (typeof value !== 'number' || typeof operand !== 'number' || !(value < operand))
return false;
break;
case '$lte':
if (typeof value !== 'number' || typeof operand !== 'number' || !(value <= operand))
return false;
break;
case '$in':
if (!Array.isArray(operand) || !operand.includes(value))
return false;
break;
case '$nin':
if (!Array.isArray(operand) || operand.includes(value))
return false;
break;
case '$exists':
if ((value !== undefined) !== operand)
return false;
break;
case '$regex':
const regex = typeof operand === 'string' ? new RegExp(operand) : operand;
if (!(regex instanceof RegExp) || !regex.test(String(value)))
return false;
break;
case '$includes':
if (!Array.isArray(value) || !value.includes(operand))
return false;
break;
case '$all':
if (!Array.isArray(value) || !Array.isArray(operand))
return false;
for (const item of operand) {
if (!value.includes(item))
return false;
}
break;
case '$size':
if (!Array.isArray(value) || value.length !== operand)
return false;
break;
default:
// Unknown operator, treat as field name
if (!matchesFieldQuery(value, op, operand))
return false;
}
}
return true;
}
/**
* Check if a field matches a query
*/
function matchesFieldQuery(obj, field, query) {
const value = getNestedValue(obj, field);
return matchesQuery(value, query);
}
/**
* Get nested value from object using dot notation
*/
function getNestedValue(obj, path) {
const parts = path.split('.');
let current = obj;
for (const part of parts) {
if (current === null || current === undefined) {
return undefined;
}
current = current[part];
}
return current;
}
/**
* Check if metadata matches the filter
*/
export function matchesMetadataFilter(metadata, filter) {
if (!filter || Object.keys(filter).length === 0) {
return true;
}
for (const [key, query] of Object.entries(filter)) {
// Handle logical operators
if (key === '$and') {
if (!Array.isArray(query))
return false;
for (const subFilter of query) {
if (!matchesMetadataFilter(metadata, subFilter))
return false;
}
continue;
}
if (key === '$or') {
if (!Array.isArray(query))
return false;
let matched = false;
for (const subFilter of query) {
if (matchesMetadataFilter(metadata, subFilter)) {
matched = true;
break;
}
}
if (!matched)
return false;
continue;
}
if (key === '$not') {
if (matchesMetadataFilter(metadata, query))
return false;
continue;
}
// Handle field queries
const value = getNestedValue(metadata, key);
if (!matchesQuery(value, query)) {
return false;
}
}
return true;
}
/**
* Calculate metadata boost score
*/
export function calculateMetadataScore(metadata, filter, scoring) {
if (!scoring || !scoring.metadataBoosts) {
return 0;
}
let score = 0;
for (const [field, boost] of Object.entries(scoring.metadataBoosts)) {
const value = getNestedValue(metadata, field);
if (typeof boost === 'function') {
score += boost(value, filter);
}
else if (value !== undefined) {
// Check if the field matches the filter
const fieldFilter = filter[field];
if (fieldFilter && matchesQuery(value, fieldFilter)) {
score += boost;
}
}
}
return score;
}
/**
* Apply compound scoring to search results
*/
export function applyCompoundScoring(results, filter, scoring) {
if (!scoring || (!scoring.vectorWeight && !scoring.metadataWeight)) {
return results;
}
const vectorWeight = scoring.vectorWeight ?? 1.0;
const metadataWeight = scoring.metadataWeight ?? 0.0;
return results.map(result => {
const metadataScore = calculateMetadataScore(result.metadata, filter, scoring);
const combinedScore = (result.score * vectorWeight) + (metadataScore * metadataWeight);
return {
...result,
score: combinedScore
};
}).sort((a, b) => b.score - a.score); // Re-sort by combined score
}
/**
* Filter search results by metadata
*/
export function filterSearchResultsByMetadata(results, filter) {
if (!filter || Object.keys(filter).length === 0) {
return results;
}
return results.filter(result => matchesMetadataFilter(result.metadata, filter));
}
/**
* Filter nouns by metadata before search
*/
export function filterNounsByMetadata(nouns, filter) {
if (!filter || Object.keys(filter).length === 0) {
return nouns;
}
return nouns.filter(noun => matchesMetadataFilter(noun.metadata, filter));
}
export function aggregateSearchResults(results, facets) {
const facetResults = {};
for (const [facetName, config] of Object.entries(facets)) {
const counts = {};
for (const result of results) {
const value = getNestedValue(result.metadata, config.field);
if (value !== undefined) {
const key = String(value);
counts[key] = (counts[key] || 0) + 1;
}
}
// Sort by count and apply limit
const sorted = Object.entries(counts)
.sort((a, b) => b[1] - a[1])
.slice(0, config.limit || 10);
facetResults[facetName] = Object.fromEntries(sorted);
}
return {
results,
facets: facetResults
};
}
//# sourceMappingURL=metadataFilter.js.map