Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 91 additions & 5 deletions azure-ai-search/lib/azure-search-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,77 @@ export class AzureSearchTools {
private searchClients: Map<string, SearchClient<any>> = new Map();
private config: AzureSearchConfig;

/**
* Removes vector fields from search results to avoid returning large vector arrays
* @param results Array of search results
* @returns Filtered results without vector fields
*/
private removeVectorFields(results: any[]): any[] {
return results.map(result => {
if (!result || typeof result !== 'object') return result;

const filteredResult = { ...result };

// Recursively filter nested objects (like document property)
this.filterVectorFieldsRecursive(filteredResult);

return filteredResult;
});
}

/**
* Recursively removes vector fields from an object
* @param obj Object to filter
*/
private filterVectorFieldsRecursive(obj: any): void {
if (!obj || typeof obj !== 'object') return;

Object.keys(obj).forEach(key => {
const value = obj[key];

// Check if this field should be removed
if (this.isVectorField(key, value)) {
delete obj[key];
return;
}

// Recursively filter nested objects
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
this.filterVectorFieldsRecursive(value);
}
});
}

/**
* Determines if a field is a vector field that should be removed
* @param key Field name
* @param value Field value
* @returns True if field should be removed
*/
private isVectorField(key: string, value: any): boolean {
const keyLower = key.toLowerCase();

// Check field name patterns
if (keyLower.includes('vector') ||
keyLower.includes('embedding') ||
key.endsWith('Vector') ||
key.endsWith('_vector') ||
key.endsWith('Embedding') ||
key.endsWith('_embedding')) {
return true;
}

// Check if it's a large numeric array (likely a vector)
if (Array.isArray(value) &&
value.length > 50 && // Lower threshold for safety
value.length < 10000 && // Reasonable upper bound for vectors
value.every((item: any) => typeof item === 'number')) {
return true;
}

return false;
}

constructor(config?: Partial<AzureSearchConfig>) {
this.config = {
endpoint: config?.endpoint || process.env.AZURE_SEARCH_ENDPOINT || "",
Expand Down Expand Up @@ -102,10 +173,13 @@ export class AzureSearchTools {
results.push(result);
}

// Remove vector fields from results
const filteredResults = this.removeVectorFields(results);

return {
success: true,
data: {
results,
results: filteredResults,
count: response.count,
facets: response.facets,
coverage: response.coverage,
Expand All @@ -132,9 +206,12 @@ export class AzureSearchTools {
selectedFields: params.select,
});

// Remove vector fields from result
const filteredResult = this.removeVectorFields([result])[0];

return {
success: true,
data: result,
data: filteredResult,
};
} catch (error) {
return {
Expand Down Expand Up @@ -502,10 +579,13 @@ export class AzureSearchTools {
results.push(result);
}

// Remove vector fields from results
const filteredResults = this.removeVectorFields(results);

return {
success: true,
data: {
results,
results: filteredResults,
count: response.count,
},
};
Expand Down Expand Up @@ -557,10 +637,13 @@ export class AzureSearchTools {
results.push(result);
}

// Remove vector fields from results
const filteredResults = this.removeVectorFields(results);

return {
success: true,
data: {
results,
results: filteredResults,
count: response.count,
facets: response.facets,
coverage: response.coverage,
Expand Down Expand Up @@ -635,10 +718,13 @@ export class AzureSearchTools {
results.push(result);
}

// Remove vector fields from results
const filteredResults = this.removeVectorFields(results);

return {
success: true,
data: {
results,
results: filteredResults,
count: response.count,
answers: (response as any).answers?.map((answer: any) => ({
key: answer.key || "",
Expand Down
2 changes: 1 addition & 1 deletion azure-ai-search/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ignitionai/azure-ai-search-mcp",
"version": "1.0.1",
"version": "1.0.2",
"description": "Complete Azure AI Search MCP server with vector search, semantic search, and document management",
"type": "module",
"bin": {
Expand Down
2 changes: 1 addition & 1 deletion azure-ai-search/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ dotenv.config();
// Create server instance
const server = new McpServer({
name: "AzureAISearchMCP",
version: "1.0.0",
version: "1.0.2",
description: "MCP server for interacting with Azure AI Search"
});

Expand Down
150 changes: 150 additions & 0 deletions azure-ai-search/test-vector-filtering.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
#!/usr/bin/env node

/**
* Test script to demonstrate vector field filtering
* This simulates the exact data structure from the LangSmith trace
*/

// Simulate the AzureSearchTools class methods for testing
class TestVectorFilter {
/**
* Determines if a field is a vector field that should be removed
*/
isVectorField(key, value) {
const keyLower = key.toLowerCase();

// Check field name patterns
if (keyLower.includes('vector') ||
keyLower.includes('embedding') ||
key.endsWith('Vector') ||
key.endsWith('_vector') ||
key.endsWith('Embedding') ||
key.endsWith('_embedding')) {
return true;
}

// Check if it's a large numeric array (likely a vector)
if (Array.isArray(value) &&
value.length > 50 && // Lower threshold for safety
value.length < 10000 && // Reasonable upper bound for vectors
value.every((item) => typeof item === 'number')) {
return true;
}

return false;
}

/**
* Recursively removes vector fields from an object
*/
filterVectorFieldsRecursive(obj) {
if (!obj || typeof obj !== 'object') return;

Object.keys(obj).forEach(key => {
const value = obj[key];

// Check if this field should be removed
if (this.isVectorField(key, value)) {
console.log(`🗑️ REMOVING vector field: "${key}" (${Array.isArray(value) ? `array of ${value.length} numbers` : typeof value})`);
delete obj[key];
return;
}

// Recursively filter nested objects
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
this.filterVectorFieldsRecursive(value);
}
});
}

/**
* Removes vector fields from search results
*/
removeVectorFields(results) {
return results.map(result => {
if (!result || typeof result !== 'object') return result;

const filteredResult = { ...result };

// Recursively filter nested objects (like document property)
this.filterVectorFieldsRecursive(filteredResult);

return filteredResult;
});
}
}

// Test data simulating the exact structure from LangSmith trace
const mockSearchResults = [
{
score: 15.204945,
document: {
chunk_id: "fc37433d5ae7_aHR0cHM6Ly9zdG9yYWdlcG9ydGZvbGlv",
parent_id: "aHR0cHM6Ly9zdG9yYWdlcG9ydGZvbGlv",
chunk: "alpha, beta et gamma) le 16 décembre 2014, Release Version 1.0.",
title: "galnet.fr_elite-dangerous-release-s1_.md",
header_1: "Elite Dangerous - Saison 1 RELEASE",
header_2: "Elite Dangerous, Saison 1",
header_3: "",
// This is the problematic field from the trace - huge vector array
text_vector: Array.from({length: 1536}, (_, i) => Math.random() * 2 - 1), // 1536 random numbers
// Additional vector fields to test
embedding_field: Array.from({length: 768}, (_, i) => Math.random()),
content_Vector: Array.from({length: 512}, (_, i) => Math.random()),
search_embedding: Array.from({length: 256}, (_, i) => Math.random())
}
},
{
score: 15.084188,
document: {
chunk_id: "38fab6c7b272_aHR0cHM6Ly9zdG9yYWdlcG9ydGZvbGlv",
chunk: "Les bêtas dans Elite: Dangerous sont toujours des moments assez 'épiques'",
title: "galnet.fr_beta-elite-dangerous-faq_.md",
text_vector: Array.from({length: 1536}, (_, i) => Math.random() * 2 - 1),
// Test edge cases
small_array: [1, 2, 3], // Should NOT be removed (too small)
large_string_array: Array.from({length: 100}, (_, i) => `item${i}`), // Should NOT be removed (not numbers)
mixed_array: [1, "text", 3] // Should NOT be removed (mixed types)
}
}
];

console.log("🧪 TESTING VECTOR FIELD FILTERING");
console.log("=" .repeat(50));

const filter = new TestVectorFilter();

console.log("\n📊 BEFORE FILTERING:");
console.log(`Result 1 - document keys: ${Object.keys(mockSearchResults[0].document).join(', ')}`);
console.log(`Result 1 - text_vector length: ${mockSearchResults[0].document.text_vector.length}`);
console.log(`Result 2 - document keys: ${Object.keys(mockSearchResults[1].document).join(', ')}`);

console.log("\n🔄 APPLYING FILTER...");
const filteredResults = filter.removeVectorFields(mockSearchResults);

console.log("\n✅ AFTER FILTERING:");
console.log(`Result 1 - document keys: ${Object.keys(filteredResults[0].document).join(', ')}`);
console.log(`Result 2 - document keys: ${Object.keys(filteredResults[1].document).join(', ')}`);

console.log("\n🎯 VERIFICATION:");
const hasVectorFields = filteredResults.some(result => {
const doc = result.document;
return Object.keys(doc).some(key =>
key.toLowerCase().includes('vector') ||
key.toLowerCase().includes('embedding') ||
(Array.isArray(doc[key]) && doc[key].length > 50 && doc[key].every(item => typeof item === 'number'))
);
});

if (hasVectorFields) {
console.log("❌ FAILED: Vector fields still present!");
} else {
console.log("✅ SUCCESS: No vector fields found in filtered results!");
}

console.log("\n📋 PRESERVED FIELDS:");
filteredResults.forEach((result, index) => {
console.log(`Result ${index + 1}:`, Object.keys(result.document).join(', '));
});

console.log("\n🎉 Test completed!");
Loading