暂存
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace app\service;
|
||||
|
||||
use support\Request;
|
||||
|
||||
class AdminAuthService
|
||||
{
|
||||
private const SESSION_KEY = 'proofdb_admin_user_id';
|
||||
|
||||
public function __construct(private readonly ?AdminUserRepository $users = null)
|
||||
{
|
||||
}
|
||||
|
||||
public function authenticate(string $username, string $password): ?array
|
||||
{
|
||||
$username = trim($username);
|
||||
if ($username === '' || $password === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = $this->users()->findByUsername($username);
|
||||
if ($user === null || !password_verify($password, $user['password_hash'])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
unset($user['password_hash']);
|
||||
return $user;
|
||||
}
|
||||
|
||||
public function login(Request $request, array $user): void
|
||||
{
|
||||
$request->session()->set(self::SESSION_KEY, (int) $user['id']);
|
||||
$this->users()->touchLastLogin((int) $user['id']);
|
||||
}
|
||||
|
||||
public function logout(Request $request): void
|
||||
{
|
||||
$request->session()->delete(self::SESSION_KEY);
|
||||
}
|
||||
|
||||
public function current(Request $request): ?array
|
||||
{
|
||||
$id = (int) $request->session()->get(self::SESSION_KEY, 0);
|
||||
if ($id <= 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$user = $this->users()->findById($id);
|
||||
if ($user === null) {
|
||||
$request->session()->delete(self::SESSION_KEY);
|
||||
return null;
|
||||
}
|
||||
|
||||
unset($user['password_hash']);
|
||||
return $user;
|
||||
}
|
||||
|
||||
private function users(): AdminUserRepository
|
||||
{
|
||||
return $this->users ?? new AdminUserRepository();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\AdminConsole;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class AdminDocService
|
||||
{
|
||||
public function __construct(private readonly ?MarkdownRenderer $renderer = null)
|
||||
{
|
||||
}
|
||||
|
||||
public function list(): array
|
||||
{
|
||||
$items = [];
|
||||
foreach (glob(base_path('apidoc/*.md')) ?: [] as $path) {
|
||||
$name = basename($path);
|
||||
$content = (string) file_get_contents($path);
|
||||
$items[] = [
|
||||
'name' => $name,
|
||||
'title' => $this->title($content, $name),
|
||||
];
|
||||
}
|
||||
|
||||
usort($items, fn (array $a, array $b): int => strcmp($a['name'], $b['name']));
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function read(string $name): array
|
||||
{
|
||||
$safeName = basename($name);
|
||||
$path = base_path('apidoc/' . $safeName);
|
||||
if (!is_file($path) || pathinfo($path, PATHINFO_EXTENSION) !== 'md') {
|
||||
throw new RuntimeException('API doc not found.');
|
||||
}
|
||||
|
||||
$content = (string) file_get_contents($path);
|
||||
return [
|
||||
'name' => $safeName,
|
||||
'title' => $this->title($content, $safeName),
|
||||
'content' => $content,
|
||||
'html' => $this->renderer()->render($content),
|
||||
];
|
||||
}
|
||||
|
||||
public function readScriptDoc(string $name): array
|
||||
{
|
||||
$safeName = basename($name);
|
||||
$path = base_path('scriptdoc/' . $safeName);
|
||||
if (!is_file($path) || pathinfo($path, PATHINFO_EXTENSION) !== 'md') {
|
||||
throw new RuntimeException('Script doc not found.');
|
||||
}
|
||||
|
||||
$content = (string) file_get_contents($path);
|
||||
return [
|
||||
'name' => $safeName,
|
||||
'title' => $this->title($content, $safeName),
|
||||
'content' => $content,
|
||||
'html' => $this->renderer()->render($content),
|
||||
];
|
||||
}
|
||||
|
||||
private function title(string $content, string $fallback): string
|
||||
{
|
||||
if (preg_match('/^#\s+(.+)$/m', $content, $matches)) {
|
||||
return trim($matches[1]);
|
||||
}
|
||||
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
private function renderer(): MarkdownRenderer
|
||||
{
|
||||
return $this->renderer ?? new MarkdownRenderer();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\AdminConsole;
|
||||
|
||||
use InvalidArgumentException;
|
||||
use support\Db;
|
||||
|
||||
class ArchiveAdminService
|
||||
{
|
||||
public function list(string $query = '', int $page = 1, int $pageSize = 20): array
|
||||
{
|
||||
$page = max(1, $page);
|
||||
$pageSize = min(100, max(1, $pageSize));
|
||||
|
||||
$builder = Db::table('archives');
|
||||
$query = trim($query);
|
||||
if ($query !== '') {
|
||||
$like = '%' . $query . '%';
|
||||
$builder->where(function ($subQuery) use ($like): void {
|
||||
$subQuery
|
||||
->orWhere('archive_uid', 'like', $like)
|
||||
->orWhere('title', 'like', $like)
|
||||
->orWhere('summary', 'like', $like)
|
||||
->orWhere('author', 'like', $like)
|
||||
->orWhere('source', 'like', $like)
|
||||
->orWhere('series', 'like', $like);
|
||||
});
|
||||
}
|
||||
|
||||
$total = (clone $builder)->count();
|
||||
$rows = $builder
|
||||
->orderByDesc('updated_time')
|
||||
->offset(($page - 1) * $pageSize)
|
||||
->limit($pageSize)
|
||||
->get([
|
||||
'archive_uid',
|
||||
'title',
|
||||
'summary',
|
||||
'year',
|
||||
'author',
|
||||
'source',
|
||||
'series',
|
||||
'tags',
|
||||
'created_time',
|
||||
'updated_time',
|
||||
Db::raw('jsonb_array_length(chunks) as chunk_count'),
|
||||
])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'items' => array_map(fn (object $row): array => $this->listItem($row), $rows),
|
||||
'total' => (int) $total,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize,
|
||||
];
|
||||
}
|
||||
|
||||
public function detail(string $archiveUid): ?array
|
||||
{
|
||||
$row = Db::table('archives')->where('archive_uid', $archiveUid)->first();
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->detailItem($row);
|
||||
}
|
||||
|
||||
public function update(string $archiveUid, array $payload): ?array
|
||||
{
|
||||
if (!$this->detail($archiveUid)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$updates = [];
|
||||
foreach (['title', 'summary', 'author', 'source', 'series', 'content', 'raw'] as $field) {
|
||||
if (array_key_exists($field, $payload)) {
|
||||
$updates[$field] = $this->nullableText($payload[$field]);
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('year', $payload)) {
|
||||
$year = trim((string) ($payload['year'] ?? ''));
|
||||
if ($year === '') {
|
||||
$updates['year'] = null;
|
||||
} elseif (!preg_match('/^\d{1,4}$/', $year)) {
|
||||
throw new InvalidArgumentException('year must be empty or a 1-4 digit number.');
|
||||
} else {
|
||||
$updates['year'] = (int) $year;
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists('tags', $payload)) {
|
||||
$updates['tags'] = json_encode($this->normalizeTags($payload['tags']), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
if (array_key_exists('metadata', $payload)) {
|
||||
$updates['metadata'] = json_encode($this->normalizeMetadata($payload['metadata']), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
if ($updates !== []) {
|
||||
Db::table('archives')->where('archive_uid', $archiveUid)->update($updates);
|
||||
}
|
||||
|
||||
return $this->detail($archiveUid);
|
||||
}
|
||||
|
||||
public function delete(string $archiveUid): bool
|
||||
{
|
||||
return (int) Db::table('archives')->where('archive_uid', $archiveUid)->delete() > 0;
|
||||
}
|
||||
|
||||
private function listItem(object $row): array
|
||||
{
|
||||
$chunks = $this->decodeJson($row->chunks ?? null, []);
|
||||
|
||||
return [
|
||||
'archive_uid' => (string) $row->archive_uid,
|
||||
'title' => $row->title,
|
||||
'summary' => $row->summary,
|
||||
'year' => $row->year === null ? null : (int) $row->year,
|
||||
'author' => $row->author,
|
||||
'source' => $row->source,
|
||||
'series' => $row->series,
|
||||
'tags' => $this->decodeJson($row->tags ?? null, []),
|
||||
'chunk_count' => property_exists($row, 'chunk_count')
|
||||
? ($row->chunk_count === null ? 0 : (int) $row->chunk_count)
|
||||
: count(is_array($chunks) ? $chunks : []),
|
||||
'created_time' => $row->created_time,
|
||||
'updated_time' => $row->updated_time,
|
||||
];
|
||||
}
|
||||
|
||||
private function detailItem(object $row): array
|
||||
{
|
||||
$data = $this->listItem($row);
|
||||
$data['metadata'] = $this->decodeJson($row->metadata ?? null, []);
|
||||
$data['content'] = $row->content;
|
||||
$data['raw'] = $row->raw;
|
||||
$data['chunks'] = $this->decodeJson($row->chunks ?? null, []);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
private function normalizeTags(mixed $value): array
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$items = $value;
|
||||
} else {
|
||||
$text = trim((string) $value);
|
||||
if ($text === '') {
|
||||
return [];
|
||||
}
|
||||
$items = preg_split('/[\r\n,]+/', $text) ?: [];
|
||||
}
|
||||
|
||||
$tags = [];
|
||||
foreach ($items as $item) {
|
||||
$tag = trim((string) $item);
|
||||
if ($tag !== '') {
|
||||
$tags[] = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
return array_values(array_unique($tags));
|
||||
}
|
||||
|
||||
private function normalizeMetadata(mixed $value): array
|
||||
{
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$text = trim((string) $value);
|
||||
if ($text === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$decoded = json_decode($text, true);
|
||||
if (!is_array($decoded)) {
|
||||
throw new InvalidArgumentException('metadata must be a JSON object or array.');
|
||||
}
|
||||
|
||||
return $decoded;
|
||||
}
|
||||
|
||||
private function nullableText(mixed $value): ?string
|
||||
{
|
||||
$text = trim((string) $value);
|
||||
return $text === '' ? null : $text;
|
||||
}
|
||||
|
||||
private function decodeJson(mixed $value, mixed $fallback): mixed
|
||||
{
|
||||
if ($value === null) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
$decoded = json_decode((string) $value, true);
|
||||
return $decoded === null && json_last_error() !== JSON_ERROR_NONE ? $fallback : $decoded;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\AdminConsole;
|
||||
|
||||
use RuntimeException;
|
||||
|
||||
class MaintenanceScriptService
|
||||
{
|
||||
private const ARG_PATTERN = '/^--[A-Za-z0-9_]+(?:=[A-Za-z0-9._:@\/-]+)?$/';
|
||||
|
||||
public function list(): array
|
||||
{
|
||||
$docs = new AdminDocService();
|
||||
$items = [];
|
||||
foreach ($this->definitions() as $definition) {
|
||||
$item = $definition;
|
||||
try {
|
||||
$doc = $docs->readScriptDoc($definition['doc_name']);
|
||||
$item['doc_title'] = $doc['title'];
|
||||
$item['doc_html'] = $doc['html'];
|
||||
$item['doc_content'] = $doc['content'];
|
||||
} catch (RuntimeException) {
|
||||
$item['doc_title'] = null;
|
||||
$item['doc_html'] = null;
|
||||
$item['doc_content'] = null;
|
||||
}
|
||||
$items[] = $item;
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function describe(string $name): array
|
||||
{
|
||||
$definitions = $this->definitions();
|
||||
if (!isset($definitions[$name])) {
|
||||
throw new RuntimeException('Script is not allowed.');
|
||||
}
|
||||
|
||||
foreach ($this->list() as $item) {
|
||||
if ($item['name'] === $name) {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
|
||||
throw new RuntimeException('Script metadata not found.');
|
||||
}
|
||||
|
||||
public function run(string $name, array $args = []): array
|
||||
{
|
||||
$definitions = $this->definitions();
|
||||
if (!isset($definitions[$name])) {
|
||||
throw new RuntimeException('Script is not allowed.');
|
||||
}
|
||||
|
||||
$script = $definitions[$name];
|
||||
$scriptPath = base_path('scripts/' . $script['file']);
|
||||
if (!is_file($scriptPath)) {
|
||||
throw new RuntimeException('Script file not found.');
|
||||
}
|
||||
|
||||
$safeArgs = [];
|
||||
foreach ($args as $arg) {
|
||||
$arg = trim((string) $arg);
|
||||
if ($arg === '') {
|
||||
continue;
|
||||
}
|
||||
if (!preg_match(self::ARG_PATTERN, $arg)) {
|
||||
throw new RuntimeException('Only --key=value style arguments are allowed.');
|
||||
}
|
||||
$safeArgs[] = $arg;
|
||||
}
|
||||
|
||||
$command = array_merge([PHP_BINARY, $scriptPath], $safeArgs);
|
||||
$descriptors = [
|
||||
0 => ['pipe', 'r'],
|
||||
1 => ['pipe', 'w'],
|
||||
2 => ['pipe', 'w'],
|
||||
];
|
||||
|
||||
$process = proc_open($command, $descriptors, $pipes, base_path());
|
||||
if (!is_resource($process)) {
|
||||
throw new RuntimeException('Failed to start script process.');
|
||||
}
|
||||
|
||||
fclose($pipes[0]);
|
||||
$stdout = (string) stream_get_contents($pipes[1]);
|
||||
fclose($pipes[1]);
|
||||
$stderr = (string) stream_get_contents($pipes[2]);
|
||||
fclose($pipes[2]);
|
||||
$exitCode = proc_close($process);
|
||||
|
||||
return [
|
||||
'script_name' => $name,
|
||||
'command' => array_merge(['php', 'scripts/' . $script['file']], $safeArgs),
|
||||
'exit_code' => $exitCode,
|
||||
'stdout' => $stdout,
|
||||
'stderr' => $stderr,
|
||||
'ok' => $exitCode === 0,
|
||||
];
|
||||
}
|
||||
|
||||
private function definitions(): array
|
||||
{
|
||||
return [
|
||||
'setup_database' => [
|
||||
'name' => 'setup_database',
|
||||
'file' => 'setup_database.php',
|
||||
'label' => '初始化数据库',
|
||||
'description' => '创建或补齐 archives、chunks 相关表结构与索引。',
|
||||
'doc_name' => 'setup_database.md',
|
||||
'args_hint' => '无参数',
|
||||
],
|
||||
'setup_opensearch' => [
|
||||
'name' => 'setup_opensearch',
|
||||
'file' => 'setup_opensearch.php',
|
||||
'label' => '初始化 OpenSearch',
|
||||
'description' => '创建或补齐 proofdb_chunks 索引与 mapping。',
|
||||
'doc_name' => 'setup_opensearch.md',
|
||||
'args_hint' => '无参数',
|
||||
],
|
||||
'reindex_opensearch' => [
|
||||
'name' => 'reindex_opensearch',
|
||||
'file' => 'reindex_opensearch.php',
|
||||
'label' => '重建 OpenSearch 索引',
|
||||
'description' => '把 PostgreSQL 中已向量化的数据重新写入 OpenSearch。',
|
||||
'doc_name' => 'reindex_opensearch.md',
|
||||
'args_hint' => '--archive_uid=01...',
|
||||
],
|
||||
'backfill_archive_content' => [
|
||||
'name' => 'backfill_archive_content',
|
||||
'file' => 'backfill_archive_content.php',
|
||||
'label' => '回填 archive content',
|
||||
'description' => '从 raw 或 chunks 回填 archives.content。',
|
||||
'doc_name' => 'backfill_archive_content.md',
|
||||
'args_hint' => '--archive_uid=01...',
|
||||
],
|
||||
'setup_admin_users' => [
|
||||
'name' => 'setup_admin_users',
|
||||
'file' => 'setup_admin_users.php',
|
||||
'label' => '初始化管理员用户',
|
||||
'description' => '创建 admin_users 表并写入或更新管理员账号。',
|
||||
'doc_name' => 'setup_admin_users.md',
|
||||
'args_hint' => '--username=admin --password=secret',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\AdminConsole;
|
||||
|
||||
class MarkdownRenderer
|
||||
{
|
||||
public function render(string $markdown): string
|
||||
{
|
||||
$lines = preg_split('/\r\n|\n|\r/', $markdown) ?: [];
|
||||
$html = [];
|
||||
$paragraph = [];
|
||||
$listType = null;
|
||||
$table = null;
|
||||
$inCodeBlock = false;
|
||||
$codeLines = [];
|
||||
|
||||
$flushParagraph = function () use (&$paragraph, &$html): void {
|
||||
if ($paragraph === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$text = implode(' ', array_map('trim', $paragraph));
|
||||
$html[] = '<p>' . $this->renderInline($text) . '</p>';
|
||||
$paragraph = [];
|
||||
};
|
||||
|
||||
$flushList = function () use (&$listType, &$html): void {
|
||||
if ($listType !== null) {
|
||||
$html[] = '</' . $listType . '>';
|
||||
$listType = null;
|
||||
}
|
||||
};
|
||||
|
||||
$flushTable = function () use (&$table, &$html): void {
|
||||
if ($table === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$html[] = '<table class="markdown-table"><thead><tr>' .
|
||||
implode('', array_map(fn (string $cell): string => '<th>' . $this->renderInline($cell) . '</th>', $table['headers'])) .
|
||||
'</tr></thead><tbody>';
|
||||
foreach ($table['rows'] as $row) {
|
||||
$html[] = '<tr>' .
|
||||
implode('', array_map(fn (string $cell): string => '<td>' . $this->renderInline($cell) . '</td>', $row)) .
|
||||
'</tr>';
|
||||
}
|
||||
$html[] = '</tbody></table>';
|
||||
$table = null;
|
||||
};
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (preg_match('/^```/', $line)) {
|
||||
$flushParagraph();
|
||||
$flushList();
|
||||
$flushTable();
|
||||
|
||||
if ($inCodeBlock) {
|
||||
$html[] = '<pre class="markdown-pre"><code>' . htmlspecialchars(implode("\n", $codeLines), ENT_QUOTES, 'UTF-8') . '</code></pre>';
|
||||
$codeLines = [];
|
||||
$inCodeBlock = false;
|
||||
} else {
|
||||
$inCodeBlock = true;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($inCodeBlock) {
|
||||
$codeLines[] = $line;
|
||||
continue;
|
||||
}
|
||||
|
||||
$trimmed = trim($line);
|
||||
if ($trimmed === '') {
|
||||
$flushParagraph();
|
||||
$flushList();
|
||||
$flushTable();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^(#{1,6})\s+(.+)$/', $trimmed, $matches)) {
|
||||
$flushParagraph();
|
||||
$flushList();
|
||||
$flushTable();
|
||||
$level = strlen($matches[1]);
|
||||
$html[] = sprintf('<h%d>%s</h%d>', $level, $this->renderInline($matches[2]), $level);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^>\s?(.+)$/', $trimmed, $matches)) {
|
||||
$flushParagraph();
|
||||
$flushList();
|
||||
$flushTable();
|
||||
$html[] = '<blockquote>' . $this->renderInline($matches[1]) . '</blockquote>';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^---+$/', $trimmed)) {
|
||||
$flushParagraph();
|
||||
$flushList();
|
||||
$flushTable();
|
||||
$html[] = '<hr>';
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($this->isTableDelimiter($trimmed) && $table !== null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (str_contains($trimmed, '|')) {
|
||||
$cells = $this->tableCells($trimmed);
|
||||
if (count($cells) >= 2) {
|
||||
$flushParagraph();
|
||||
$flushList();
|
||||
if ($table === null) {
|
||||
$table = ['headers' => $cells, 'rows' => []];
|
||||
} else {
|
||||
$table['rows'][] = $cells;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (preg_match('/^[-*]\s+(.+)$/', $trimmed, $matches)) {
|
||||
$flushParagraph();
|
||||
$flushTable();
|
||||
if ($listType !== 'ul') {
|
||||
$flushList();
|
||||
$listType = 'ul';
|
||||
$html[] = '<ul>';
|
||||
}
|
||||
$html[] = '<li>' . $this->renderInline($matches[1]) . '</li>';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (preg_match('/^\d+\.\s+(.+)$/', $trimmed, $matches)) {
|
||||
$flushParagraph();
|
||||
$flushTable();
|
||||
if ($listType !== 'ol') {
|
||||
$flushList();
|
||||
$listType = 'ol';
|
||||
$html[] = '<ol>';
|
||||
}
|
||||
$html[] = '<li>' . $this->renderInline($matches[1]) . '</li>';
|
||||
continue;
|
||||
}
|
||||
|
||||
$flushList();
|
||||
$flushTable();
|
||||
$paragraph[] = $trimmed;
|
||||
}
|
||||
|
||||
if ($inCodeBlock) {
|
||||
$html[] = '<pre class="markdown-pre"><code>' . htmlspecialchars(implode("\n", $codeLines), ENT_QUOTES, 'UTF-8') . '</code></pre>';
|
||||
}
|
||||
|
||||
$flushParagraph();
|
||||
$flushList();
|
||||
$flushTable();
|
||||
|
||||
return implode("\n", $html);
|
||||
}
|
||||
|
||||
private function renderInline(string $text): string
|
||||
{
|
||||
$text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
|
||||
$text = preg_replace('/`([^`]+)`/', '<code>$1</code>', $text) ?? $text;
|
||||
$text = preg_replace('/\*\*([^*]+)\*\*/', '<strong>$1</strong>', $text) ?? $text;
|
||||
$text = preg_replace('/\*([^*]+)\*/', '<em>$1</em>', $text) ?? $text;
|
||||
$text = preg_replace('/\[(.+?)\]\((.+?)\)/', '<a href="$2" target="_blank" rel="noreferrer">$1</a>', $text) ?? $text;
|
||||
|
||||
return $text;
|
||||
}
|
||||
|
||||
private function isTableDelimiter(string $line): bool
|
||||
{
|
||||
return (bool) preg_match('/^\|?[\s:-]+\|[\s|:-]*$/', $line);
|
||||
}
|
||||
|
||||
private function tableCells(string $line): array
|
||||
{
|
||||
$line = trim($line);
|
||||
$line = trim($line, '|');
|
||||
return array_map(static fn (string $cell): string => trim($cell), explode('|', $line));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
<?php
|
||||
|
||||
namespace app\service\AdminConsole;
|
||||
|
||||
use app\service\Search\OpenSearchClientFactory;
|
||||
use support\Db;
|
||||
use Throwable;
|
||||
|
||||
class OpenSearchAdminService
|
||||
{
|
||||
public function status(): array
|
||||
{
|
||||
$config = config('opensearch.default', []);
|
||||
$indexName = config('opensearch.indices.chunks', 'proofdb_chunks');
|
||||
|
||||
$status = [
|
||||
'config' => [
|
||||
'hosts' => $config['hosts'] ?? [],
|
||||
'ssl_verify' => (bool) ($config['ssl_verify'] ?? true),
|
||||
'index_name' => $indexName,
|
||||
],
|
||||
'database' => [
|
||||
'archives_total' => (int) Db::table('archives')->count(),
|
||||
'chunks_total' => (int) Db::table('chunks')->count(),
|
||||
'embedded_chunks' => (int) Db::table('chunks')->where('embedding_status', 3)->count(),
|
||||
'indexed_chunks' => (int) Db::table('chunks')->where('search_index_status', 3)->count(),
|
||||
],
|
||||
'opensearch' => [
|
||||
'reachable' => false,
|
||||
'index_exists' => false,
|
||||
'cluster_name' => null,
|
||||
'health' => null,
|
||||
'docs_count' => 0,
|
||||
'mapping_fields' => [],
|
||||
'error' => null,
|
||||
],
|
||||
];
|
||||
|
||||
try {
|
||||
$client = (new OpenSearchClientFactory())->make();
|
||||
$health = $client->cluster()->health();
|
||||
$status['opensearch']['reachable'] = true;
|
||||
$status['opensearch']['cluster_name'] = $health['cluster_name'] ?? null;
|
||||
$status['opensearch']['health'] = $health['status'] ?? null;
|
||||
|
||||
$exists = (bool) $client->indices()->exists(['index' => $indexName]);
|
||||
$status['opensearch']['index_exists'] = $exists;
|
||||
|
||||
if ($exists) {
|
||||
$stats = $client->indices()->stats(['index' => $indexName]);
|
||||
$mapping = $client->indices()->getMapping(['index' => $indexName]);
|
||||
$status['opensearch']['docs_count'] = (int) (($stats['_all']['primaries']['docs']['count'] ?? 0));
|
||||
$status['opensearch']['mapping_fields'] = array_keys($mapping[$indexName]['mappings']['properties'] ?? []);
|
||||
}
|
||||
} catch (Throwable $exception) {
|
||||
$status['opensearch']['error'] = $exception->getMessage();
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
public function documents(string $query = '', int $size = 20): array
|
||||
{
|
||||
$size = min(50, max(1, $size));
|
||||
$indexName = config('opensearch.indices.chunks', 'proofdb_chunks');
|
||||
$client = (new OpenSearchClientFactory())->make();
|
||||
|
||||
if (!(bool) $client->indices()->exists(['index' => $indexName])) {
|
||||
return [
|
||||
'index_name' => $indexName,
|
||||
'items' => [],
|
||||
'total' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$body = [
|
||||
'_source' => [
|
||||
'includes' => [
|
||||
'chunk_uid',
|
||||
'archive_uid',
|
||||
'chunk_index',
|
||||
'page_start',
|
||||
'page_end',
|
||||
'title',
|
||||
'summary',
|
||||
'source',
|
||||
'author',
|
||||
'year',
|
||||
'series',
|
||||
'tags',
|
||||
'text',
|
||||
'embedding_model',
|
||||
'embedding_dimensions',
|
||||
'created_time',
|
||||
'updated_time',
|
||||
],
|
||||
],
|
||||
'size' => $size,
|
||||
'sort' => [
|
||||
['updated_time' => ['order' => 'desc']],
|
||||
],
|
||||
];
|
||||
|
||||
$query = trim($query);
|
||||
if ($query === '') {
|
||||
$body['query'] = ['match_all' => (object) []];
|
||||
} else {
|
||||
$body['query'] = [
|
||||
'multi_match' => [
|
||||
'query' => $query,
|
||||
'fields' => ['text^3', 'title^2', 'summary^2', 'source', 'author', 'tags'],
|
||||
'type' => 'best_fields',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
$response = $client->search([
|
||||
'index' => $indexName,
|
||||
'body' => $body,
|
||||
]);
|
||||
|
||||
$hits = $response['hits']['hits'] ?? [];
|
||||
|
||||
return [
|
||||
'index_name' => $indexName,
|
||||
'total' => (int) (($response['hits']['total']['value'] ?? 0)),
|
||||
'items' => array_map(function (array $hit): array {
|
||||
$source = $hit['_source'] ?? [];
|
||||
$text = trim((string) ($source['text'] ?? ''));
|
||||
return [
|
||||
'score' => $hit['_score'] ?? null,
|
||||
'chunk_uid' => $source['chunk_uid'] ?? ($hit['_id'] ?? null),
|
||||
'archive_uid' => $source['archive_uid'] ?? null,
|
||||
'chunk_index' => $source['chunk_index'] ?? null,
|
||||
'page_start' => $source['page_start'] ?? null,
|
||||
'page_end' => $source['page_end'] ?? null,
|
||||
'title' => $source['title'] ?? null,
|
||||
'summary' => $source['summary'] ?? null,
|
||||
'source' => $source['source'] ?? null,
|
||||
'author' => $source['author'] ?? null,
|
||||
'year' => $source['year'] ?? null,
|
||||
'series' => $source['series'] ?? null,
|
||||
'tags' => $source['tags'] ?? [],
|
||||
'text_preview' => mb_substr($text, 0, 320),
|
||||
'embedding_model' => $source['embedding_model'] ?? null,
|
||||
'embedding_dimensions' => $source['embedding_dimensions'] ?? null,
|
||||
'created_time' => $source['created_time'] ?? null,
|
||||
'updated_time' => $source['updated_time'] ?? null,
|
||||
];
|
||||
}, $hits),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
<?php
|
||||
|
||||
namespace app\service;
|
||||
|
||||
use support\Db;
|
||||
|
||||
class AdminUserRepository
|
||||
{
|
||||
public function listAll(): array
|
||||
{
|
||||
$rows = Db::table('admin_users')
|
||||
->orderByDesc('id')
|
||||
->get()
|
||||
->all();
|
||||
|
||||
return array_map(fn (object $row): array => $this->toArray($row), $rows);
|
||||
}
|
||||
|
||||
public function findByUsername(string $username): ?array
|
||||
{
|
||||
$row = Db::table('admin_users')
|
||||
->where('username', $username)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
return $row ? $this->toArray($row) : null;
|
||||
}
|
||||
|
||||
public function findById(int $id): ?array
|
||||
{
|
||||
$row = Db::table('admin_users')
|
||||
->where('id', $id)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
return $row ? $this->toArray($row) : null;
|
||||
}
|
||||
|
||||
public function findAnyById(int $id): ?array
|
||||
{
|
||||
$row = Db::table('admin_users')->where('id', $id)->first();
|
||||
return $row ? $this->toArray($row) : null;
|
||||
}
|
||||
|
||||
public function findAnyByUsername(string $username): ?array
|
||||
{
|
||||
$row = Db::table('admin_users')->where('username', $username)->first();
|
||||
return $row ? $this->toArray($row) : null;
|
||||
}
|
||||
|
||||
public function touchLastLogin(int $id): void
|
||||
{
|
||||
Db::table('admin_users')
|
||||
->where('id', $id)
|
||||
->update(['last_login_at' => Db::raw('CURRENT_TIMESTAMP')]);
|
||||
}
|
||||
|
||||
public function create(string $username, string $password, ?string $displayName = null): array
|
||||
{
|
||||
$id = Db::table('admin_users')->insertGetId([
|
||||
'username' => $username,
|
||||
'display_name' => $displayName,
|
||||
'password_hash' => password_hash($password, PASSWORD_DEFAULT),
|
||||
'is_active' => true,
|
||||
'last_login_at' => null,
|
||||
]);
|
||||
|
||||
return $this->findAnyById((int) $id) ?? [];
|
||||
}
|
||||
|
||||
public function updateUser(int $id, array $fields): ?array
|
||||
{
|
||||
$updates = [];
|
||||
|
||||
if (array_key_exists('display_name', $fields)) {
|
||||
$displayName = $fields['display_name'];
|
||||
$updates['display_name'] = $displayName === null ? null : trim((string) $displayName);
|
||||
}
|
||||
|
||||
if (array_key_exists('password', $fields) && trim((string) $fields['password']) !== '') {
|
||||
$updates['password_hash'] = password_hash((string) $fields['password'], PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
if (array_key_exists('is_active', $fields)) {
|
||||
$updates['is_active'] = (bool) $fields['is_active'];
|
||||
}
|
||||
|
||||
if ($updates !== []) {
|
||||
Db::table('admin_users')->where('id', $id)->update($updates);
|
||||
}
|
||||
|
||||
return $this->findAnyById($id);
|
||||
}
|
||||
|
||||
private function toArray(object $row): array
|
||||
{
|
||||
return [
|
||||
'id' => (int) $row->id,
|
||||
'username' => (string) $row->username,
|
||||
'display_name' => $row->display_name,
|
||||
'password_hash' => (string) $row->password_hash,
|
||||
'is_active' => (bool) $row->is_active,
|
||||
'last_login_at' => $row->last_login_at,
|
||||
'created_time' => $row->created_time,
|
||||
'updated_time' => $row->updated_time,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,102 @@ class ArchiveRepository
|
||||
return implode("\n\n", array_map(fn ($chunk): string => (string) $chunk->text, $chunks));
|
||||
}
|
||||
|
||||
public function findChunk(string $chunkUid): ?array
|
||||
{
|
||||
$row = Db::table('chunks')
|
||||
->join('archives', 'chunks.archive_uid', '=', 'archives.archive_uid')
|
||||
->where('chunks.chunk_uid', $chunkUid)
|
||||
->first([
|
||||
'chunks.chunk_uid',
|
||||
'chunks.archive_uid',
|
||||
'chunks.chunk_index',
|
||||
'chunks.page_start',
|
||||
'chunks.page_end',
|
||||
'chunks.text',
|
||||
'chunks.length',
|
||||
'chunks.embedding_status',
|
||||
'chunks.embedding_ref',
|
||||
'chunks.embedding_model',
|
||||
'chunks.embedding_error',
|
||||
'chunks.search_index_status',
|
||||
'chunks.search_index_error',
|
||||
'archives.title',
|
||||
'archives.summary',
|
||||
'archives.year',
|
||||
'archives.author',
|
||||
'archives.source',
|
||||
'archives.series',
|
||||
'archives.tags',
|
||||
'archives.metadata',
|
||||
]);
|
||||
|
||||
if (!$row) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'chunk_uid' => (string) $row->chunk_uid,
|
||||
'archive_uid' => (string) $row->archive_uid,
|
||||
'chunk_index' => (int) $row->chunk_index,
|
||||
'page_start' => $row->page_start === null ? null : (int) $row->page_start,
|
||||
'page_end' => $row->page_end === null ? null : (int) $row->page_end,
|
||||
'pages' => $this->pages($row->page_start, $row->page_end),
|
||||
'text' => (string) $row->text,
|
||||
'length' => $row->length === null ? null : (int) $row->length,
|
||||
'embedding_status' => (int) $row->embedding_status,
|
||||
'embedding_ref' => $this->decodeJson($row->embedding_ref ?? null, null),
|
||||
'embedding_model' => $row->embedding_model,
|
||||
'embedding_error' => $row->embedding_error,
|
||||
'search_index_status' => (int) $row->search_index_status,
|
||||
'search_index_error' => $row->search_index_error,
|
||||
'archive' => [
|
||||
'archive_uid' => (string) $row->archive_uid,
|
||||
'title' => $row->title,
|
||||
'summary' => $row->summary,
|
||||
'year' => $row->year === null ? null : (int) $row->year,
|
||||
'author' => $row->author,
|
||||
'source' => $row->source,
|
||||
'series' => $row->series,
|
||||
'tags' => $this->decodeJson($row->tags ?? null, []),
|
||||
'metadata' => $this->decodeJson($row->metadata ?? null, []),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function findArchiveChunks(string $archiveUid): array
|
||||
{
|
||||
$rows = Db::table('chunks')
|
||||
->join('archives', 'chunks.archive_uid', '=', 'archives.archive_uid')
|
||||
->where('chunks.archive_uid', $archiveUid)
|
||||
->orderBy('chunks.chunk_index')
|
||||
->get([
|
||||
'chunks.chunk_uid',
|
||||
'chunks.archive_uid',
|
||||
'chunks.chunk_index',
|
||||
'chunks.page_start',
|
||||
'chunks.page_end',
|
||||
'chunks.text',
|
||||
'chunks.length',
|
||||
'chunks.embedding_status',
|
||||
'chunks.embedding_ref',
|
||||
'chunks.embedding_model',
|
||||
'chunks.embedding_error',
|
||||
'chunks.search_index_status',
|
||||
'chunks.search_index_error',
|
||||
'archives.title',
|
||||
'archives.summary',
|
||||
'archives.year',
|
||||
'archives.author',
|
||||
'archives.source',
|
||||
'archives.series',
|
||||
'archives.tags',
|
||||
'archives.metadata',
|
||||
])
|
||||
->all();
|
||||
|
||||
return array_map(fn (object $row): array => $this->chunkRowToArray($row), $rows);
|
||||
}
|
||||
|
||||
public function updateMetadata(string $archiveUid, array $fields, array $aiMeta): void
|
||||
{
|
||||
$archive = $this->findArchive($archiveUid);
|
||||
@@ -136,4 +232,68 @@ class ArchiveRepository
|
||||
'chunks' => json_decode($archive->chunks ?? '[]', true) ?: [],
|
||||
];
|
||||
}
|
||||
|
||||
private function chunkRowToArray(object $row): array
|
||||
{
|
||||
return [
|
||||
'chunk_uid' => (string) $row->chunk_uid,
|
||||
'archive_uid' => (string) $row->archive_uid,
|
||||
'chunk_index' => (int) $row->chunk_index,
|
||||
'page_start' => $row->page_start === null ? null : (int) $row->page_start,
|
||||
'page_end' => $row->page_end === null ? null : (int) $row->page_end,
|
||||
'pages' => $this->pages($row->page_start, $row->page_end),
|
||||
'text' => (string) $row->text,
|
||||
'length' => $row->length === null ? null : (int) $row->length,
|
||||
'embedding_status' => (int) $row->embedding_status,
|
||||
'embedding_ref' => $this->decodeJson($row->embedding_ref ?? null, null),
|
||||
'embedding_model' => $row->embedding_model,
|
||||
'embedding_error' => $row->embedding_error,
|
||||
'search_index_status' => (int) $row->search_index_status,
|
||||
'search_index_error' => $row->search_index_error,
|
||||
'archive' => [
|
||||
'archive_uid' => (string) $row->archive_uid,
|
||||
'title' => $row->title,
|
||||
'summary' => $row->summary,
|
||||
'year' => $row->year === null ? null : (int) $row->year,
|
||||
'author' => $row->author,
|
||||
'source' => $row->source,
|
||||
'series' => $row->series,
|
||||
'tags' => $this->decodeJson($row->tags ?? null, []),
|
||||
'metadata' => $this->decodeJson($row->metadata ?? null, []),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
private function decodeJson(mixed $value, mixed $fallback): mixed
|
||||
{
|
||||
if ($value === null) {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (!is_string($value) || trim($value) === '') {
|
||||
return $fallback;
|
||||
}
|
||||
|
||||
$decoded = json_decode($value, true);
|
||||
return $decoded === null && json_last_error() !== JSON_ERROR_NONE ? $fallback : $decoded;
|
||||
}
|
||||
|
||||
private function pages(mixed $pageStart, mixed $pageEnd): array
|
||||
{
|
||||
if (!is_numeric($pageStart) || !is_numeric($pageEnd)) {
|
||||
return array_values(array_filter([$pageStart, $pageEnd], static fn ($value): bool => $value !== null && $value !== ''));
|
||||
}
|
||||
|
||||
$start = (int) $pageStart;
|
||||
$end = (int) $pageEnd;
|
||||
if ($end < $start) {
|
||||
$end = $start;
|
||||
}
|
||||
|
||||
return range($start, $end);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -70,6 +70,16 @@ class ArticleImportService
|
||||
}
|
||||
}
|
||||
|
||||
public function normalizeArchiveContentString(string $content): ?string
|
||||
{
|
||||
return $this->nullableClean($this->cleanMarkdownPage($content));
|
||||
}
|
||||
|
||||
public function normalizeArchiveRawString(string $content): ?string
|
||||
{
|
||||
return $this->nullableClean($content);
|
||||
}
|
||||
|
||||
private function validate(array $payload): array
|
||||
{
|
||||
$errors = [];
|
||||
@@ -182,8 +192,8 @@ class ArticleImportService
|
||||
'tags' => is_array($payload['tags'] ?? null) ? array_values($payload['tags']) : [],
|
||||
'summary' => $this->nullableClean($payload['summary'] ?? null),
|
||||
'metadata' => $payload['metadata'] ?? [],
|
||||
'content' => $this->nullableClean($payload['content_url'] ?? $payload['content_path'] ?? null),
|
||||
'raw' => $this->nullableClean($payload['raw_url'] ?? $payload['raw_path'] ?? null),
|
||||
'content' => $this->normalizedArchiveContent($payload),
|
||||
'raw' => $this->rawArchiveContent($payload),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -200,6 +210,57 @@ class ArticleImportService
|
||||
return $this->pageBlocksFromItems($payload, preg_split('/\R{2,}/u', $payload['content']));
|
||||
}
|
||||
|
||||
private function normalizedArchiveContent(array $payload): ?string
|
||||
{
|
||||
if (isset($payload['pages']) && is_array($payload['pages'])) {
|
||||
$parts = [];
|
||||
foreach ($payload['pages'] as $page) {
|
||||
if (!is_array($page) || !isset($page['content']) || !is_string($page['content'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$content = $this->cleanMarkdownPage($page['content']);
|
||||
if ($content !== '') {
|
||||
$parts[] = $content;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->nullableClean(implode("\n\n", $parts));
|
||||
}
|
||||
|
||||
if (isset($payload['paragraphs']) && is_array($payload['paragraphs'])) {
|
||||
$parts = [];
|
||||
foreach ($payload['paragraphs'] as $paragraph) {
|
||||
$content = is_array($paragraph) ? ($paragraph['content'] ?? '') : $paragraph;
|
||||
if (!is_string($content)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$content = $this->clean($content);
|
||||
if ($content !== '') {
|
||||
$parts[] = $content;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->nullableClean(implode("\n\n", $parts));
|
||||
}
|
||||
|
||||
if (isset($payload['content']) && is_string($payload['content'])) {
|
||||
return $this->normalizeArchiveContentString($payload['content']);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function rawArchiveContent(array $payload): ?string
|
||||
{
|
||||
if (isset($payload['content']) && is_string($payload['content'])) {
|
||||
return $this->normalizeArchiveRawString($payload['content']);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private function pageBlocksFromPages(array $payload): array
|
||||
{
|
||||
$pageBlocks = [];
|
||||
|
||||
@@ -7,6 +7,22 @@ use support\Db;
|
||||
|
||||
class ChunkSearchIndexRepository
|
||||
{
|
||||
public function resetEmbeddedChunksToPending(?string $archiveUid = null): int
|
||||
{
|
||||
$query = Db::table('chunks')
|
||||
->where('embedding_status', EmbeddingStatus::EMBEDDED);
|
||||
|
||||
if ($archiveUid !== null && trim($archiveUid) !== '') {
|
||||
$query->where('archive_uid', trim($archiveUid));
|
||||
}
|
||||
|
||||
return $query->update([
|
||||
'search_index_status' => SearchIndexStatus::PENDING,
|
||||
'search_index_error' => null,
|
||||
'search_index_updated_at' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function queuePendingArchiveTasks(int $limit): array
|
||||
{
|
||||
$statuses = [
|
||||
@@ -63,6 +79,7 @@ class ChunkSearchIndexRepository
|
||||
'chunks.created_time',
|
||||
'chunks.updated_time',
|
||||
'archives.title',
|
||||
'archives.summary',
|
||||
'archives.source',
|
||||
'archives.author',
|
||||
'archives.year',
|
||||
@@ -105,6 +122,7 @@ class ChunkSearchIndexRepository
|
||||
'page_start' => $row->page_start === null ? null : (int) $row->page_start,
|
||||
'page_end' => $row->page_end === null ? null : (int) $row->page_end,
|
||||
'title' => $row->title,
|
||||
'summary' => $row->summary,
|
||||
'source' => $row->source,
|
||||
'author' => $row->author,
|
||||
'year' => $row->year === null ? null : (int) $row->year,
|
||||
|
||||
@@ -16,6 +16,7 @@ class OpenSearchChunkIndex
|
||||
$index = $this->indexName();
|
||||
|
||||
if ($client->indices()->exists(['index' => $index])) {
|
||||
$this->ensureProperties($client, $index);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -64,6 +65,7 @@ class OpenSearchChunkIndex
|
||||
'page_start' => ['type' => 'integer'],
|
||||
'page_end' => ['type' => 'integer'],
|
||||
'title' => $this->textWithKeyword(),
|
||||
'summary' => ['type' => 'text'],
|
||||
'source' => $this->textWithKeyword(),
|
||||
'author' => $this->textWithKeyword(),
|
||||
'year' => ['type' => 'integer'],
|
||||
@@ -93,6 +95,31 @@ class OpenSearchChunkIndex
|
||||
return $this->client ?? (new OpenSearchClientFactory())->make();
|
||||
}
|
||||
|
||||
private function ensureProperties(Client $client, string $index): void
|
||||
{
|
||||
$mapping = $client->indices()->getMapping(['index' => $index]);
|
||||
$existing = $mapping[$index]['mappings']['properties'] ?? [];
|
||||
$desired = $this->mapping()['mappings']['properties'] ?? [];
|
||||
$missing = [];
|
||||
|
||||
foreach ($desired as $field => $definition) {
|
||||
if (!array_key_exists($field, $existing)) {
|
||||
$missing[$field] = $definition;
|
||||
}
|
||||
}
|
||||
|
||||
if ($missing === []) {
|
||||
return;
|
||||
}
|
||||
|
||||
$client->indices()->putMapping([
|
||||
'index' => $index,
|
||||
'body' => [
|
||||
'properties' => $missing,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
private function indexName(): string
|
||||
{
|
||||
return config('opensearch.indices.chunks', 'proofdb_chunks');
|
||||
|
||||
@@ -39,6 +39,7 @@ class OpenSearchSearchService
|
||||
'fields' => [
|
||||
'text^4',
|
||||
'title^3',
|
||||
'summary^2',
|
||||
'source^2',
|
||||
'author^2',
|
||||
'series^2',
|
||||
@@ -219,6 +220,7 @@ class OpenSearchSearchService
|
||||
'page_start' => $source['page_start'] ?? null,
|
||||
'page_end' => $source['page_end'] ?? null,
|
||||
'title' => $source['title'] ?? null,
|
||||
'summary' => $source['summary'] ?? null,
|
||||
'source' => $source['source'] ?? null,
|
||||
'author' => $source['author'] ?? null,
|
||||
'year' => $source['year'] ?? null,
|
||||
@@ -322,6 +324,7 @@ class OpenSearchSearchService
|
||||
'page_start',
|
||||
'page_end',
|
||||
'title',
|
||||
'summary',
|
||||
'source',
|
||||
'author',
|
||||
'year',
|
||||
|
||||
Reference in New Issue
Block a user