Переехали на графовую модель, засунули все в докер, поменяли парсинг

This commit is contained in:
2026-07-23 08:34:26 +04:00
parent c503fc4990
commit b2a13528ee
14 changed files with 420 additions and 204 deletions

View File

@@ -0,0 +1,139 @@
using CodeBase.Models;
using Neo4j.Driver;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class GraphRepository
{
private readonly IDriver _driver;
public GraphRepository(IDriver driver)
{
_driver = driver;
}
// ==========================================
// 0. ИНИЦИАЛИЗАЦИЯ (Создаем пространство для векторов)
// ==========================================
public async Task InitializeDbAsync()
{
await using var session = _driver.AsyncSession();
await session.ExecuteWriteAsync(async tx =>
{
// Говорим базе: "Создай индекс для поиска по сходству, если его еще нет.
// Размер вектора 768 (GraphCodeBERT), алгоритм - косинусное расстояние"
await tx.RunAsync(@"
CREATE VECTOR INDEX code_vectors IF NOT EXISTS
FOR (c:CodeChunk) ON (c.vector)
OPTIONS { indexConfig: {
`vector.dimensions`: 768,
`vector.similarity_function`: 'cosine'
}}"
);
});
}
// ==========================================
// 1. СОХРАНЕНИЕ УЗЛОВ И ВЕКТОРОВ
// ==========================================
public async Task SaveChunksAsync(string projectName, List<CodeChunk> chunks)
{
await using var session = _driver.AsyncSession();
// 1. Делаем данные "безопасными" для базы
var parameters = chunks.Where(c => c.Vector != null).Select(chunk => new
{
id = Guid.NewGuid().ToString(),
projectName = projectName,
filePath = chunk.FilePath,
methodName = chunk.MethodName,
content = chunk.Content,
vector = chunk.Vector,
// Защита от null и пустых строк (база их не переварит в цикле FOREACH)
outgoingCalls = chunk.OutgoingCalls != null
? chunk.OutgoingCalls.Where(x => !string.IsNullOrWhiteSpace(x)).ToList()
: new List<string>()
}).ToList();
await session.ExecuteWriteAsync(async tx =>
{
await tx.RunAsync(@"
UNWIND $batch AS chunk
// Создаем или обновляем основной метод
MERGE (c:CodeChunk { methodName: chunk.methodName })
SET c.id = chunk.id,
c.projectName = chunk.projectName,
c.filePath = chunk.filePath,
c.content = chunk.content,
c.vector = chunk.vector
// Рисуем связи только для валидных вызовов
FOREACH (calledMethod IN chunk.outgoingCalls |
MERGE (target:CodeChunk { methodName: calledMethod })
MERGE (c)-[:CALLS]->(target)
)
", new { batch = parameters });
});
}
// ==========================================
// 2. ПОИСК (Пока только по вектору, связи добавим позже)
// ==========================================
public async Task<List<GraphNodeContext>> SearchAsync(string projectName, float[] queryVector, int topK = 15)
{
await using var session = _driver.AsyncSession();
return await session.ExecuteReadAsync(async tx =>
{
var cursor = await tx.RunAsync(@"
// 1. Ищем самые подходящие узлы по векторному сходству
CALL db.index.vector.queryNodes('code_vectors', $topK, $queryVector)
YIELD node AS c, score
WHERE c.projectName = $projectName
// 2. Ищем стрелочки ВНИЗ (Кого вызывает этот метод?)
// OPTIONAL MATCH гарантирует, что мы не потеряем узел, если связей нет
OPTIONAL MATCH (c)-[:CALLS]->(out:CodeChunk)
// Собираем имена вызываемых методов в массив
WITH c, score, collect(DISTINCT out.methodName) AS outgoingDependencies
// 3. Ищем стрелочки ВВЕРХ (Кто вызывает этот метод?)
OPTIONAL MATCH (in:CodeChunk)-[:CALLS]->(c)
WITH c, score, outgoingDependencies, collect(DISTINCT in.methodName) AS incomingDependencies
// 4. Возвращаем готовую структуру для C#
RETURN
c.methodName AS methodName,
c.filePath AS filePath,
c.content AS content,
outgoingDependencies,
incomingDependencies,
score
ORDER BY score DESC
", new { topK, queryVector, projectName });
var results = new List<GraphNodeContext>();
while (await cursor.FetchAsync())
{
var record = cursor.Current;
results.Add(new GraphNodeContext
{
MethodName = record["methodName"].As<string>(),
FilePath = record["filePath"].As<string>(),
Content = record["content"].As<string>(),
// Драйвер Neo4j возвращает массивы как IList<object>,
// поэтому аккуратно кастуем их в наши списки строк
OutgoingDependencies = record["outgoingDependencies"].As<IList<string>>().ToList(),
IncomingDependencies = record["incomingDependencies"].As<IList<string>>().ToList()
});
}
return results;
});
}
}

View File

@@ -1,89 +0,0 @@
using CodeBase.Models;
using Qdrant.Client;
using Qdrant.Client.Grpc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
public class QdrantRepository
{
private readonly QdrantClient _client;
private const string CollectionName = "CodeBaseCollection";
public QdrantRepository(QdrantClient client)
{
_client = client;
}
// ==========================================
// 1. СОХРАНЕНИЕ ВЕКТОРОВ С ПРИВЯЗКОЙ К ПРОЕКТУ
// ==========================================
public async Task SaveChunksAsync(string projectName, List<CodeChunk> chunks)
{
var points = new List<PointStruct>();
foreach (var chunk in chunks)
{
if (chunk.Vector == null) continue;
// Формируем точку для БД
var point = new PointStruct
{
Id = Guid.NewGuid(), // Уникальный ID чанка
Vectors = chunk.Vector, // Тот самый массив float[]
// Вот здесь мы навешиваем все ярлыки!
Payload =
{
["ProjectName"] = projectName,
["FilePath"] = chunk.FilePath,
["MethodName"] = chunk.MethodName,
["Content"] = chunk.Content // Сам текст кода тоже храним прямо в БД
}
};
points.Add(point);
}
// Отправляем пачку векторов в базу
await _client.UpsertAsync(CollectionName, points);
}
// ==========================================
// 2. ПОИСК ТОЛЬКО В РАМКАХ ОДНОГО ПРОЕКТА
// ==========================================
public async Task<List<CodeChunk>> SearchAsync(string projectName, float[] queryVector, int topK = 15)
{
// Создаем фильтр: искать только там, где ProjectName совпадает с запрошенным
var projectFilter = new Filter
{
Must = {
new Condition
{
Field = new FieldCondition
{
Key = "ProjectName",
Match = new Match { Keyword = projectName }
}
}
}
};
// База делает всю тяжелую математику за нас!
var searchResults = await _client.SearchAsync(
collectionName: CollectionName,
vector: queryVector,
limit: (ulong)topK,
filter: projectFilter, // Передаем наш фильтр
payloadSelector: true // Просим БД вернуть Payload вместе с результатами
);
// Превращаем ответы базы обратно в наши C#-объекты CodeChunk
return searchResults.Select(hit => new CodeChunk
{
FilePath = hit.Payload["FilePath"].StringValue,
MethodName = hit.Payload["MethodName"].StringValue,
Content = hit.Payload["Content"].StringValue
}).ToList();
}
}