Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .cspell/custom-dictionary-workspace.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6086,3 +6086,22 @@ zupinnovation
Zwirner
zyngacareers
zyngaearlycareers

orfao
indice
indices
hidrata
hidratacao
scraper
srem
scard
estagio
sunionstore
compativel
perfil
vagas
paginacao
empresa
senioridade
modalidade
localizacao
19 changes: 18 additions & 1 deletion LOCAL_DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -444,12 +444,29 @@ Ele não deve ser utilizado para criar dados de produção.

O seed cria:

* 1 usuário comum
* 1 usuário comum (com perfil técnico preenchido: React, TypeScript, Node.js, PostgreSQL, Docker e AWS)
* 1 usuário administrador
* 2 registros de preferências
* 3 vagas salvas
* 2 notas privadas
* 2 eventos de alteração de candidatura
* 12 vagas de catálogo no Valkey, exibidas na aba `/vagas`

## Vagas da aba `/vagas`

A aba `/vagas` não lê do PostgreSQL: ela consome o catálogo que o scraper grava
no Valkey (`scraper:job:<id>` mais os índices invertidos `scraper:jobs:*`). Para
que a tela tenha conteúdo sem rodar o scraper, o seed grava 12 vagas em
`backend/src/scripts/seedCatalogJobs.ts`, junto com os mesmos índices de
tecnologia, família, senioridade, nível, modelo, contrato e localidade.

As vagas foram montadas para casar com o perfil técnico do usuário `dev` em
ordem decrescente de match — de 99% (todas as tecnologias do perfil) a 45%
(nenhuma tecnologia em comum). Com o filtro **Maior match** na aba, elas
aparecem exatamente nessa ordem.

Esse trecho é ignorado com um aviso quando `VALKEY_URL` não está definida, e a
ordenação é coberta por `backend/tests/unit/scripts/seedCatalogJobs.test.ts`.

---

Expand Down
75 changes: 55 additions & 20 deletions backend/src/lib/cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,14 +141,7 @@ export async function cacheAbsoluteSCard(absoluteKey: string): Promise<number> {
}
}

/**
* Realiza uma busca cruzada (Interseção) entre múltiplos índices de palavras-chave no Valkey.
* Se apenas uma palavra-chave for enviada, retorna os membros dela diretamente.
*
* Normalização espelha o Go:
* "UX/UI Designer" → "ux ui designer" → chave: scraper:jobs:keyword:ux ui designer
* "UI" → "ui" → chave: scraper:jobs:keyword:ui
*/

export async function cacheSearchKeywords(
keywords: string[],
): Promise<string[]> {
Expand Down Expand Up @@ -376,10 +369,24 @@ export async function cacheSearchJobIds(
}
}

export async function cacheGetJobsByIds(ids: string[]): Promise<unknown[]> {
export type CacheJobsByIdsResult = {
/** Vagas encontradas no Valkey. */
jobs: unknown[];
/** IDs do indice sem documento `scraper:job:<id>` (orfaos). */
missingIds: string[];
};

/**
* Hidrata os documentos das vagas a partir dos IDs do indice e informa quais
* IDs estao orfaos. O indice global e gravado sem TTL pelo scraper enquanto os
* documentos expiram em 9 dias (scraper-go/internal/pipeline/pipeline.go).
*/
export async function cacheGetJobsByIdsDetailed(
ids: string[],
): Promise<CacheJobsByIdsResult> {
const client = await getCache();

if (ids.length === 0) return [];
if (ids.length === 0) return { jobs: [], missingIds: [] };

const keys = ids.map((id) => `scraper:job:${id}`);
let results: Array<string | null>;
Expand All @@ -390,21 +397,49 @@ export async function cacheGetJobsByIds(ids: string[]): Promise<unknown[]> {
throw error;
}

const jobs = results
.filter((raw): raw is string => raw !== null)
.map((raw) => {
try {
return JSON.parse(raw);
} catch {
return null;
}
})
.filter(Boolean);
const jobs: unknown[] = [];
const missingIds: string[] = [];

results.forEach((raw, index) => {
if (raw === null) {
missingIds.push(ids[index]);
return;
}

try {
jobs.push(JSON.parse(raw));
} catch {
missingIds.push(ids[index]);
}
});

recordCacheOperation("mget_jobs", jobs.length > 0 ? "hit" : "miss");
return { jobs, missingIds };
}

export async function cacheGetJobsByIds(ids: string[]): Promise<unknown[]> {
const { jobs } = await cacheGetJobsByIdsDetailed(ids);
return jobs;
}

/**
* Remove IDs orfaos do indice global, espelhando o auto-reparo ja feito pelo
* scraper Go (jobstore.GetAll/GetSample).
*/
export async function cacheRemoveJobIndexIds(ids: string[]): Promise<number> {
if (ids.length === 0) return 0;

const client = await getCache();
try {
const removed = await client.sRem("scraper:jobs:index", ids);
recordCacheOperation("srem_orphan_jobs", "ok");
return removed;
} catch (error) {
recordCacheOperation("srem_orphan_jobs", "error");
throw error;
}
}

async function cacheDeleteByPattern(pattern: string): Promise<number> {
const client = await getCache();
let cursor = "0";
Expand Down
Loading
Loading