Elasticsearch Knowledge Graph for MCP
by j3k0
- legacy
import { describe, it, expect } from 'vitest';
import { parseQuery, filterEntitiesByQuery, createEntitySearchItems, scoreAndSortEntities, createFilteredGraph, searchGraph } from './query-language.js';
import { Entity, KnowledgeGraph } from './types.js';
describe('Query Language', () => {
describe('parseQuery', () => {
it('should parse a complex query correctly', () => {
const query = 'type:person +programmer -manager frontend|backend|fullstack name:john free text';
const result = parseQuery(query);
expect(result.type).toBe('person');
expect(result.name).toBe('john');
expect(result.include).toContain('programmer');
expect(result.exclude).toContain('manager');
expect(result.or).toHaveLength(1);
expect(result.or[0]).toContain('frontend');
expect(result.or[0]).toContain('backend');
expect(result.or[0]).toContain('fullstack');
expect(result.freeText).toBe('free text');
});
it('should handle empty queries', () => {
const result = parseQuery('');
expect(result.freeText).toBe('');
expect(result.type).toBeNull();
expect(result.name).toBeNull();
expect(result.include).toHaveLength(0);
expect(result.exclude).toHaveLength(0);
expect(result.or).toHaveLength(0);
});
it('should parse multiple includes and excludes', () => {
const query = '+first +second -exclude1 -exclude2';
const result = parseQuery(query);
expect(result.include).toHaveLength(2);
expect(result.include).toContain('first');
expect(result.include).toContain('second');
expect(result.exclude).toHaveLength(2);
expect(result.exclude).toContain('exclude1');
expect(result.exclude).toContain('exclude2');
});
it('should parse multiple OR groups', () => {
const query = 'group1|group2 apple|orange|banana';
const result = parseQuery(query);
expect(result.or).toHaveLength(2);
// Order might be reversed due to processing matches in reverse order
const orGroups = result.or.map(group => group.sort().join(','));
expect(orGroups).toContain('group1,group2');
expect(orGroups).toContain('apple,banana,orange');
});
});
describe('filterEntitiesByQuery', () => {
const entities: Entity[] = [
{ name: 'John Smith', entityType: 'person', observations: ['programmer', 'likes coffee', 'works remote'] },
{ name: 'Jane Doe', entityType: 'person', observations: ['manager', 'likes tea', 'office worker'] },
{ name: 'React', entityType: 'technology', observations: ['frontend', 'javascript library', 'UI development'] },
{ name: 'Node.js', entityType: 'technology', observations: ['backend', 'javascript runtime', 'server-side'] },
];
const entitySearchItems = createEntitySearchItems(entities);
it('should filter by type', () => {
const parsedQuery = parseQuery('type:person');
const results = filterEntitiesByQuery(entitySearchItems, parsedQuery);
expect(results).toHaveLength(2);
expect(results.map(item => item.entity.name)).toContain('John Smith');
expect(results.map(item => item.entity.name)).toContain('Jane Doe');
});
it('should filter by name', () => {
const parsedQuery = parseQuery('name:john');
const results = filterEntitiesByQuery(entitySearchItems, parsedQuery);
expect(results).toHaveLength(1);
expect(results[0].entity.name).toBe('John Smith');
});
it('should apply AND logic with include terms', () => {
const parsedQuery = parseQuery('+programmer +coffee');
const results = filterEntitiesByQuery(entitySearchItems, parsedQuery);
expect(results).toHaveLength(1);
expect(results[0].entity.name).toBe('John Smith');
});
it('should apply NOT logic with exclude terms', () => {
const parsedQuery = parseQuery('type:person -manager');
const results = filterEntitiesByQuery(entitySearchItems, parsedQuery);
expect(results).toHaveLength(1);
expect(results[0].entity.name).toBe('John Smith');
});
it('should apply OR logic correctly', () => {
const parsedQuery = parseQuery('frontend|backend');
const results = filterEntitiesByQuery(entitySearchItems, parsedQuery);
expect(results).toHaveLength(2);
expect(results.map(item => item.entity.name)).toContain('React');
expect(results.map(item => item.entity.name)).toContain('Node.js');
});
it('should apply fuzzy search for free text', () => {
const parsedQuery = parseQuery('jvs'); // fuzzy matching for "javascript"
const results = filterEntitiesByQuery(entitySearchItems, parsedQuery);
expect(results).toHaveLength(2);
expect(results.map(item => item.entity.name)).toContain('React');
expect(results.map(item => item.entity.name)).toContain('Node.js');
});
it('should combine all filter types in complex queries', () => {
const parsedQuery = parseQuery('type:person +programmer -manager coffee|tea');
const results = filterEntitiesByQuery(entitySearchItems, parsedQuery);
expect(results).toHaveLength(1);
expect(results[0].entity.name).toBe('John Smith');
});
});
describe('scoreAndSortEntities', () => {
const entities: Entity[] = [
{ name: 'javascript', entityType: 'language', observations: ['programming language', 'web development'] },
{ name: 'java', entityType: 'language', observations: ['programming language', 'enterprise'] },
{ name: 'python', entityType: 'language', observations: ['programming language', 'data science'] },
{ name: 'typescript', entityType: 'language', observations: ['superset of javascript', 'types'] },
];
const entitySearchItems = createEntitySearchItems(entities);
it('should score exact name matches highest', () => {
const parsedQuery = parseQuery('java');
const filtered = filterEntitiesByQuery(entitySearchItems, parsedQuery);
const results = scoreAndSortEntities(filtered, parsedQuery);
// 'java' should be scored highest as exact match
expect(results[0].entity.name).toBe('java');
});
it('should score partial name matches higher than content-only matches', () => {
const parsedQuery = parseQuery('javascript');
const filtered = filterEntitiesByQuery(entitySearchItems, parsedQuery);
const results = scoreAndSortEntities(filtered, parsedQuery);
// Order should be: 'javascript' (exact), 'typescript' (partial), 'others'
expect(results[0].entity.name).toBe('javascript');
// typescript contains javascript in name, so should be second
expect(results[1].entity.name).toBe('typescript');
});
});
describe('createFilteredGraph', () => {
it('should filter relations to include those where either entity is in the filtered set', () => {
const entities: Entity[] = [
{ name: 'A', entityType: 'letter', observations: ['first letter'] },
{ name: 'B', entityType: 'letter', observations: ['second letter'] },
{ name: 'C', entityType: 'letter', observations: ['third letter'] },
];
// Only include A and B
const filteredEntities = entities.filter(e => ['A', 'B'].includes(e.name));
// Convert to ScoredEntity format for createFilteredGraph
const scoredEntities = filteredEntities.map(entity => ({ entity, score: 1.0 }));
const graph = createFilteredGraph(scoredEntities);
expect(graph.scoredEntities).toHaveLength(2);
});
});
describe('searchGraph', () => {
const testGraph: KnowledgeGraph = {
entities: [
{ name: 'John Smith', entityType: 'person', observations: ['programmer', 'likes coffee', 'works remote'] },
{ name: 'Jane Doe', entityType: 'person', observations: ['manager', 'likes tea', 'office worker'] },
{ name: 'React', entityType: 'technology', observations: ['frontend', 'javascript library', 'UI development'] },
{ name: 'Node.js', entityType: 'technology', observations: ['backend', 'javascript runtime', 'server-side'] },
],
relations: [
{ from: 'John Smith', to: 'React', relationType: 'uses' },
{ from: 'John Smith', to: 'Node.js', relationType: 'uses' },
{ from: 'Jane Doe', to: 'React', relationType: 'manages_project' },
]
};
it('should return the full graph for empty query', () => {
const result = searchGraph('', testGraph);
expect(result.scoredEntities).toHaveLength(testGraph.entities.length);
});
it('should perform a full search with filtering and sorting', () => {
const result = searchGraph('type:person +programmer', testGraph);
expect(result.scoredEntities).toHaveLength(1);
expect(result.scoredEntities[0].entity.name).toBe('John Smith');
});
it('should maintain relationships between matched entities', () => {
const result = searchGraph('+javascript', testGraph);
expect(result.scoredEntities).toHaveLength(2);
expect(result.scoredEntities.map(e => e.entity.name)).toContain('React');
expect(result.scoredEntities.map(e => e.entity.name)).toContain('Node.js');
});
});
});