117 lines
3.4 KiB
Dart
117 lines
3.4 KiB
Dart
import 'dart:convert';
|
|
|
|
import 'package:capstone_project/utils/agrigator.dart';
|
|
import 'package:http/http.dart' as http;
|
|
|
|
|
|
const _baseUrl = 'http://duriin.imbenji.net';
|
|
|
|
|
|
// pair of (article, semantic distance from the query / seed). distance is
|
|
// null when the underlying endpoint doesnt return one.
|
|
class SimilarHit {
|
|
final FeedItem item;
|
|
final double? distance;
|
|
|
|
SimilarHit({required this.item, this.distance});
|
|
}
|
|
|
|
|
|
class DuriinService {
|
|
|
|
Future<List<FeedItem>> search({
|
|
required String ticker,
|
|
required String companyName,
|
|
required DateTime from,
|
|
required DateTime to,
|
|
int limit = 100,
|
|
}) async {
|
|
final fromStr = from.toUtc().toIso8601String();
|
|
final toStr = to.toUtc().toIso8601String();
|
|
|
|
|
|
// build the keyword set: the ticker itself plus the company brand name
|
|
// (first token of the registered name — "NVIDIA Corporation" -> "NVIDIA",
|
|
// "Apple Inc" -> "Apple"). the full legal name rarely appears verbatim in
|
|
// articles so matching on it would miss most hits.
|
|
final keywords = <String>{};
|
|
final t = ticker.trim();
|
|
if (t.isNotEmpty) keywords.add(t);
|
|
|
|
final brand = companyName.trim().split(RegExp(r"\s+")).firstOrNull ?? "";
|
|
if (brand.isNotEmpty) keywords.add(brand);
|
|
|
|
final hits = await _fetchHits(queryParams: {
|
|
'keyword': keywords.toList(),
|
|
'keyword_mode': 'or',
|
|
'from': fromStr,
|
|
'to': toStr,
|
|
'limit': '$limit',
|
|
'order': 'newest',
|
|
});
|
|
|
|
final items = hits.map((h) => h.item).toList();
|
|
|
|
print('Duriin: ${items.length} articles for $ticker / $companyName (kw=${keywords.join(",")})');
|
|
return items;
|
|
}
|
|
|
|
|
|
// neighbours of a given article via the vector index. used by the event
|
|
// clusterer to build real clusters instead of arbitrary time slices.
|
|
Future<List<SimilarHit>> findSimilar(int articleId, {int limit = 25}) async {
|
|
return _fetchHits(queryParams: {
|
|
'similar_to_article': '$articleId',
|
|
'limit': '$limit',
|
|
});
|
|
}
|
|
|
|
|
|
Future<List<SimilarHit>> _fetchHits({required Map<String, dynamic> queryParams}) async {
|
|
final uri = Uri.parse(_baseUrl).replace(
|
|
path: '/articles',
|
|
queryParameters: queryParams,
|
|
);
|
|
|
|
try {
|
|
final response = await http.get(uri);
|
|
|
|
if (response.statusCode != 200) {
|
|
print('Duriin: HTTP ${response.statusCode} for $uri');
|
|
return [];
|
|
}
|
|
|
|
final body = response.body.trim();
|
|
if (body.isEmpty) return [];
|
|
|
|
final list = jsonDecode(body) as List<dynamic>;
|
|
|
|
return list.map((raw) {
|
|
final m = raw as Map<String, dynamic>;
|
|
|
|
final id = m['id'] is int
|
|
? m['id'] as int
|
|
: int.tryParse('${m['id']}');
|
|
|
|
final item = FeedItem(
|
|
id: id,
|
|
title: (m['title'] ?? '').toString(),
|
|
description: (m['description'] ?? '').toString(),
|
|
content: (m['content'] ?? '').toString(),
|
|
link: (m['url'] ?? '').toString(),
|
|
source: (m['source'] ?? '').toString().isNotEmpty ? m['source'].toString() : null,
|
|
pubDate: m['pub_date'] != null ? DateTime.tryParse(m['pub_date'].toString()) : null,
|
|
);
|
|
|
|
final distRaw = m['distance'];
|
|
final distance = distRaw is num ? distRaw.toDouble() : null;
|
|
|
|
return SimilarHit(item: item, distance: distance);
|
|
}).where((h) => h.item.title.isNotEmpty && h.item.link.isNotEmpty).toList();
|
|
|
|
} catch (e) {
|
|
print('Duriin fetch error: $e');
|
|
return [];
|
|
}
|
|
}
|
|
}
|