Search requests #2
@@ -22,6 +22,12 @@ const DATA_LIST_SUFFIX = /\s*—\s*данные\s*$/;
|
||||
// свежесть); при поиске по тексту эта подстраховка не применяется (см. RequestsTableView).
|
||||
const RECENT_MS = 10 * 60 * 1000;
|
||||
|
||||
// Сколько строк согласования максимум забираем из Search за один запрос
|
||||
// (см. getInvolvedRowsViaSearch). Постраничного перебора здесь нет: набор
|
||||
// целиком уходит в слияние и сортировку по дате заявки, поэтому при превышении
|
||||
// лимита пишем предупреждение в лог, а не молча обрезаем.
|
||||
const SEARCH_ROW_LIMIT = 500;
|
||||
|
||||
export function currentUserEmail(context: WebPartContext): string {
|
||||
return (context.pageContext.user.email || '').toLowerCase();
|
||||
}
|
||||
@@ -181,20 +187,19 @@ async function indexApprovalsForType(
|
||||
|
||||
if (scope === 'myTurn') {
|
||||
// 'myTurn' — гибрид для масштаба (список согласования может быть очень
|
||||
// большим): Search находит кандидатов по managed property ApproverOWSUSER
|
||||
// без сканирования списка целиком, плюс REST по «последним N минутам»
|
||||
// (Modified) добирает то, что Search мог ещё не проиндексировать. Status
|
||||
// в обоих случаях берётся СВЕЖИМ через REST по найденным Id (не из индекса
|
||||
// Search) — именно это делает getInvolvedRowsViaSearch, поэтому решение
|
||||
// «моя очередь сейчас» всегда точное, даже когда список кандидатов пришёл
|
||||
// из Search.
|
||||
// большим): Search находит кандидатов, не сканируя список целиком, плюс
|
||||
// REST по «последним N минутам» (Modified) добирает то, что Search мог ещё
|
||||
// не проиндексировать. Status в обоих случаях берётся СВЕЖИМ через REST по
|
||||
// найденным Id (не из индекса Search) — именно это делает
|
||||
// getInvolvedRowsViaSearch, поэтому решение «моя очередь сейчас» всегда
|
||||
// точное, даже когда список кандидатов пришёл из Search.
|
||||
const cutoffIso = new Date(Date.now() - RECENT_MS).toISOString();
|
||||
const recentRows = (await approvalList.items
|
||||
.filter(`ApproverId eq ${approver.Id} and Modified ge '${cutoffIso}'`)
|
||||
.select('Id', 'TicketId', 'Status')
|
||||
.top(2000)()) as Array<Record<string, unknown>>;
|
||||
|
||||
const searchRows = await getInvolvedRowsViaSearch(context, type, approvalList, claimsLogin);
|
||||
const searchRows = await getInvolvedRowsViaSearch(context, type, approvalList);
|
||||
const seenIds = new Set(recentRows.map((r) => r.Id));
|
||||
const allRows = recentRows.concat(searchRows.filter((r) => !seenIds.has(r.Id)));
|
||||
|
||||
@@ -207,12 +212,12 @@ async function indexApprovalsForType(
|
||||
} else {
|
||||
// 'involved' — используется ТОЛЬКО для текстового поиска (см.
|
||||
// getPendingApprovalIndex/searchMyRequests) — здесь важна не мгновенная
|
||||
// свежесть, а полнота по всему архиву, поэтому ищем чисто через Search
|
||||
// по managed property ApproverOWSUSER, без REST-подстраховки на
|
||||
// «последние N минут» — гибрид REST+Search приводил к неустойчивому
|
||||
// результату (заявка то находилась, то нет, в зависимости от того,
|
||||
// успел ли Search её проиндексировать к моменту REST-окна).
|
||||
stepRows = await getInvolvedRowsViaSearch(context, type, approvalList, claimsLogin);
|
||||
// свежесть, а полнота по всему архиву, поэтому только Search, без
|
||||
// REST-подстраховки на «последние N минут»: гибрид REST+Search при поиске
|
||||
// давал неустойчивый результат (заявка то находилась, то нет, в
|
||||
// зависимости от того, успел ли Search её проиндексировать к моменту
|
||||
// REST-окна).
|
||||
stepRows = await getInvolvedRowsViaSearch(context, type, approvalList);
|
||||
|
||||
console.log(
|
||||
`[Текущие заявки] «${type.name}»: список согласования «${approvalListTitle}», approver.Id=${approver.Id}, scope=involved (Search) — найдено: ${stepRows.length}`
|
||||
@@ -254,21 +259,36 @@ async function indexApprovalsForType(
|
||||
}
|
||||
}
|
||||
|
||||
// 'involved' целиком — через Search по managed property ApproverOWSUSER
|
||||
// (кастомное Person-поле, НЕТ гарантии, что проиндексировано без ручной
|
||||
// настройки Search Schema — если лог покажет «найдено: 0» при заведомо
|
||||
// непустом архиве, значит не сработало).
|
||||
// 'involved' — строки согласования текущего пользователя через Search, чтобы не
|
||||
// сканировать список целиком (он растёт с каждой заявкой).
|
||||
//
|
||||
// Search используется ТОЛЬКО чтобы найти Id строк — TicketId и Status берём
|
||||
// свежими через REST по этим Id (точечное чтение по известным Id — не поиск,
|
||||
// а обычное дочитывание данных, как и везде в проекте для найденных Search'ом строк).
|
||||
// Ищем ОТОБРАЖАЕМЫМ ИМЕНЕМ СВОБОДНЫМ ТЕРМОМ, а не через managed property
|
||||
// ApproverOWSUSER. Подтверждено диагностикой 2026-08-05 в этом тенанте:
|
||||
// ApproverOWSUSER отдаёт null у всех строк (автосозданного свойства не было —
|
||||
// нашлось только чужое ApprooverOWSUSER с опечаткой; созданное вручную и
|
||||
// смапленное на ows_Approver так и не наполнилось), а вот свободный терм
|
||||
// `"Павел Горбенко"` находит именно свои 12 строк из 27. То есть Person-поле в
|
||||
// полнотекстовый индекс попадает как текст, просто через managed property не
|
||||
// достаётся.
|
||||
//
|
||||
// Search используется ТОЛЬКО чтобы сузить набор Id — TicketId и Status берём
|
||||
// свежими через REST по этим Id, а не из индекса (индекс мог устареть).
|
||||
async function getInvolvedRowsViaSearch(
|
||||
context: WebPartContext,
|
||||
type: IRequestType,
|
||||
approvalList: IList,
|
||||
claimsLogin: string
|
||||
approvalList: IList
|
||||
): Promise<Array<Record<string, unknown>>> {
|
||||
try {
|
||||
// Кавычки сломали бы фразу в KQL — убираем (в ФИО их быть не должно, но
|
||||
// значение приходит извне, поэтому не доверяем).
|
||||
const displayName = (context.pageContext.user.displayName || '').replace(/"/g, ' ').trim();
|
||||
if (!displayName) {
|
||||
console.warn(
|
||||
`[Текущие заявки] «${type.name}»: у текущего пользователя нет отображаемого имени — Search по согласующему невозможен`
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const rootFolder = (await approvalList
|
||||
.select('RootFolder/ServerRelativeUrl')
|
||||
.expand('RootFolder')()) as { RootFolder?: { ServerRelativeUrl?: string } };
|
||||
@@ -277,21 +297,28 @@ async function getInvolvedRowsViaSearch(
|
||||
const approvalListAbsUrl = `${siteOrigin(type.siteUrl as string)}${relUrl}`;
|
||||
|
||||
const sp = spfi(type.siteUrl as string).using(SPFx(context));
|
||||
const kql = `Path:"${approvalListAbsUrl}*" AND ApproverOWSUSER:"${claimsLogin}"`;
|
||||
const kql = `Path:"${approvalListAbsUrl}*" AND "${displayName}"`;
|
||||
|
||||
const results = await sp.search({
|
||||
Querytext: kql,
|
||||
RowLimit: 500,
|
||||
RowLimit: SEARCH_ROW_LIMIT,
|
||||
SelectProperties: ['ListItemID'],
|
||||
TrimDuplicates: false,
|
||||
});
|
||||
|
||||
console.log(`[Текущие заявки] Search «${type.name}» (архив, involved): KQL = ${kql}, найдено: ${results.TotalRows}`);
|
||||
console.table(results.PrimarySearchResults);
|
||||
|
||||
const total = results.TotalRows || 0;
|
||||
const ids = (results.PrimarySearchResults as Array<Record<string, unknown>>)
|
||||
.map((r) => Number(r.ListItemID))
|
||||
.filter((id) => !isNaN(id) && id > 0);
|
||||
|
||||
console.log(
|
||||
`[Текущие заявки] Search «${type.name}» (involved): KQL = ${kql}, найдено: ${total}, годных Id: ${ids.length}`
|
||||
);
|
||||
if (total > SEARCH_ROW_LIMIT) {
|
||||
console.warn(
|
||||
`[Текущие заявки] «${type.name}»: Search вернул ${total} строк, взяли первые ${SEARCH_ROW_LIMIT} — остальные не попадут в выборку`
|
||||
);
|
||||
}
|
||||
if (ids.length === 0) return [];
|
||||
|
||||
return (await approvalList.items
|
||||
@@ -300,7 +327,7 @@ async function getInvolvedRowsViaSearch(
|
||||
.top(ids.length)()) as Array<Record<string, unknown>>;
|
||||
} catch (e) {
|
||||
console.error(
|
||||
`[Текущие заявки] «${type.name}»: Search по архиву согласования не сработал:`,
|
||||
`[Текущие заявки] «${type.name}»: Search по списку согласования не сработал:`,
|
||||
(e as Error).message
|
||||
);
|
||||
return [];
|
||||
|
||||
Reference in New Issue
Block a user