Переехали на графовую модель, засунули все в докер, поменяли парсинг
This commit is contained in:
139
CodeBase/Repositories/GraphRepository.cs
Normal file
139
CodeBase/Repositories/GraphRepository.cs
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user