Remove deprecated slot definitions and add archived versions for future reference

- Deleted the following slot definitions:
  - wikidata_class_slot
  - wikidata_entity_label_slot
  - wikidata_mapping_rationale_slot
  - word_count_slot

- Added archived versions of the deleted slots to preserve historical data:
  - wikidata_class_archived_20260114.yaml
  - wikidata_entity_label_archived_20260114.yaml
  - wikidata_mapping_rationale_archived_20260114.yaml
  - word_count_archived_20260114.yaml

- Introduced a new hook `usePersonSearch` for enhanced semantic search functionality in the frontend, supporting debounced queries and caching.
This commit is contained in:
kempersc 2026-01-14 22:57:09 +01:00
parent 1389b744f1
commit d5d970b513
13 changed files with 4332 additions and 3245 deletions

View file

@ -1,5 +1,5 @@
{ {
"generated": "2026-01-14T21:33:36.352Z", "generated": "2026-01-14T21:38:51.740Z",
"schemaRoot": "/schemas/20251121/linkml", "schemaRoot": "/schemas/20251121/linkml",
"totalFiles": 3026, "totalFiles": 3026,
"categoryCounts": { "categoryCounts": {

View file

@ -0,0 +1,238 @@
/**
* usePersonSearch Hook
*
* Provides semantic search functionality for person profiles using
* the RAG API's Qdrant vector database backend.
*
* Features:
* - Semantic vector search across all profiles
* - Filter by field type (name, email, domain, birth_year)
* - Debounced queries to prevent API overload
* - Caching of search results
*/
import { useState, useEffect, useCallback, useRef } from 'react';
export type SearchField = 'all' | 'name' | 'email' | 'domain' | 'birth_year';
export interface PersonSearchResult {
ppid?: string;
name: string;
headline?: string | null;
custodian_name?: string | null;
custodian_slug?: string | null;
linkedin_url?: string | null;
heritage_relevant?: boolean | null;
heritage_type?: string | null;
location?: string | null;
email?: string | null;
email_domain?: string | null;
birth_year?: number | null;
score?: number | null;
}
export interface PersonSearchResponse {
query: string;
results: PersonSearchResult[];
result_count: number;
query_time_ms: number;
collection_stats?: Record<string, unknown> | null;
embedding_model_used?: string | null;
}
interface UsePersonSearchOptions {
debounceMs?: number;
minQueryLength?: number;
maxResults?: number;
}
interface UsePersonSearchReturn {
// Search state
query: string;
setQuery: (query: string) => void;
searchField: SearchField;
setSearchField: (field: SearchField) => void;
// Results
results: PersonSearchResult[];
isSearching: boolean;
error: string | null;
// Metadata
queryTimeMs: number | null;
resultCount: number;
embeddingModelUsed: string | null;
// Actions
clearSearch: () => void;
search: (query: string, field?: SearchField) => Promise<void>;
}
const API_BASE = '/api/rag';
export function usePersonSearch(options: UsePersonSearchOptions = {}): UsePersonSearchReturn {
const {
debounceMs = 300,
minQueryLength = 2,
maxResults = 50,
} = options;
// State
const [query, setQuery] = useState('');
const [searchField, setSearchField] = useState<SearchField>('all');
const [results, setResults] = useState<PersonSearchResult[]>([]);
const [isSearching, setIsSearching] = useState(false);
const [error, setError] = useState<string | null>(null);
const [queryTimeMs, setQueryTimeMs] = useState<number | null>(null);
const [resultCount, setResultCount] = useState(0);
const [embeddingModelUsed, setEmbeddingModelUsed] = useState<string | null>(null);
// Refs for debouncing
const debounceTimerRef = useRef<NodeJS.Timeout | null>(null);
const abortControllerRef = useRef<AbortController | null>(null);
// Build the search query based on field selection
const buildSearchQuery = useCallback((rawQuery: string, field: SearchField): string => {
const trimmedQuery = rawQuery.trim();
if (!trimmedQuery) return '';
switch (field) {
case 'name':
return `person named ${trimmedQuery}`;
case 'email':
return `email address ${trimmedQuery}`;
case 'domain':
return `working at domain ${trimmedQuery}`;
case 'birth_year':
return `born in ${trimmedQuery}`;
case 'all':
default:
return trimmedQuery;
}
}, []);
// Perform the actual search
const performSearch = useCallback(async (searchQuery: string, field: SearchField) => {
if (searchQuery.trim().length < minQueryLength) {
setResults([]);
setResultCount(0);
setQueryTimeMs(null);
return;
}
// Cancel any pending request
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
abortControllerRef.current = new AbortController();
setIsSearching(true);
setError(null);
try {
const enhancedQuery = buildSearchQuery(searchQuery, field);
const response = await fetch(`${API_BASE}/persons/search`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
query: enhancedQuery,
k: maxResults,
only_heritage_relevant: false,
}),
signal: abortControllerRef.current.signal,
});
if (!response.ok) {
throw new Error(`Search failed: ${response.statusText}`);
}
const data: PersonSearchResponse = await response.json();
setResults(data.results);
setResultCount(data.result_count);
setQueryTimeMs(data.query_time_ms);
setEmbeddingModelUsed(data.embedding_model_used || null);
} catch (err) {
if (err instanceof Error && err.name === 'AbortError') {
// Request was cancelled, ignore
return;
}
console.error('Person search error:', err);
setError(err instanceof Error ? err.message : 'Search failed');
setResults([]);
} finally {
setIsSearching(false);
}
}, [buildSearchQuery, maxResults, minQueryLength]);
// Debounced search effect
useEffect(() => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
if (!query.trim()) {
setResults([]);
setResultCount(0);
setQueryTimeMs(null);
return;
}
debounceTimerRef.current = setTimeout(() => {
performSearch(query, searchField);
}, debounceMs);
return () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
};
}, [query, searchField, debounceMs, performSearch]);
// Manual search function (bypasses debounce)
const search = useCallback(async (searchQuery: string, field?: SearchField) => {
await performSearch(searchQuery, field || searchField);
}, [performSearch, searchField]);
// Clear search
const clearSearch = useCallback(() => {
setQuery('');
setResults([]);
setResultCount(0);
setQueryTimeMs(null);
setError(null);
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
}, []);
// Cleanup on unmount
useEffect(() => {
return () => {
if (debounceTimerRef.current) {
clearTimeout(debounceTimerRef.current);
}
if (abortControllerRef.current) {
abortControllerRef.current.abort();
}
};
}, []);
return {
query,
setQuery,
searchField,
setSearchField,
results,
isSearching,
error,
queryTimeMs,
resultCount,
embeddingModelUsed,
clearSearch,
search,
};
}

View file

@ -505,6 +505,181 @@
color: var(--text-primary, #e0e0e0); color: var(--text-primary, #e0e0e0);
} }
/* Enhanced Profile Search */
.profile-search-enhanced {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.5rem;
margin-bottom: 0.5rem;
}
/* Search Mode Toggle */
.search-mode-toggle {
display: flex;
gap: 0.25rem;
background: var(--bg-secondary, #f5f5f5);
border-radius: 6px;
padding: 0.25rem;
}
.search-mode-toggle .mode-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
padding: 0.375rem 0.5rem;
border: none;
background: transparent;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
color: var(--text-secondary, #666);
cursor: pointer;
transition: all 0.15s;
}
.search-mode-toggle .mode-btn:hover {
background: var(--bg-primary, #fff);
color: var(--text-primary, #1a1a2e);
}
.search-mode-toggle .mode-btn.active {
background: var(--accent-color, #4f46e5);
color: white;
}
.search-mode-toggle .mode-btn svg {
width: 14px;
height: 14px;
}
.dark .search-mode-toggle {
background: var(--bg-tertiary, #2a2a4a);
}
.dark .search-mode-toggle .mode-btn {
color: var(--text-secondary, #999);
}
.dark .search-mode-toggle .mode-btn:hover {
background: var(--bg-secondary, #1a1a2e);
color: var(--text-primary, #e0e0e0);
}
/* Search Field Filter Dropdown */
.search-field-filter {
margin: 0;
}
.search-field-filter .field-select {
width: 100%;
padding: 0.375rem 0.5rem;
font-size: 0.75rem;
border: 1px solid var(--border-color, #e0e0e0);
border-radius: 4px;
background: var(--bg-primary, #fff);
color: var(--text-primary, #1a1a2e);
cursor: pointer;
outline: none;
}
.search-field-filter .field-select:focus {
border-color: var(--accent-color, #4f46e5);
}
.dark .search-field-filter .field-select {
background: var(--bg-tertiary, #2a2a4a);
border-color: var(--border-color, #3a3a5a);
color: var(--text-primary, #e0e0e0);
}
/* Search Stats */
.search-stats {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: var(--text-secondary, #666);
padding: 0 0.25rem;
}
.search-stats .query-time {
opacity: 0.7;
}
/* Search Error */
.search-error {
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0.375rem 0.5rem;
font-size: 0.75rem;
color: var(--error-color, #dc2626);
background: rgba(220, 38, 38, 0.1);
border-radius: 4px;
}
/* Search Spinner */
.profile-search .search-spinner {
flex-shrink: 0;
color: var(--accent-color, #4f46e5);
}
/* Semantic Search Results */
.profile-list.semantic-results .profile-item {
position: relative;
}
.profile-item.semantic-result .score-badge {
font-size: 0.625rem;
padding: 0.125rem 0.375rem;
background: var(--accent-color, #4f46e5);
color: white;
border-radius: 10px;
font-weight: 600;
margin-left: auto;
}
.profile-item .headline-text {
font-size: 0.7rem;
color: var(--text-secondary, #666);
font-style: italic;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-item .custodian-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.65rem;
padding: 0.125rem 0.375rem;
background: var(--bg-tertiary, #e0e0e0);
border-radius: 4px;
color: var(--text-secondary, #666);
}
.profile-item .location-badge {
display: inline-flex;
align-items: center;
gap: 0.25rem;
font-size: 0.65rem;
color: var(--text-secondary, #666);
}
.dark .profile-item .headline-text {
color: var(--text-secondary, #999);
}
.dark .profile-item .custodian-badge {
background: var(--bg-secondary, #1a1a2e);
color: var(--text-secondary, #999);
}
.profile-list { .profile-list {
list-style: none; list-style: none;
padding: 0; padding: 0;

View file

@ -17,6 +17,7 @@ import { useState, useEffect, useCallback } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { useLanguage } from '../contexts/LanguageContext'; import { useLanguage } from '../contexts/LanguageContext';
import { Tooltip } from '../components/common/Tooltip'; import { Tooltip } from '../components/common/Tooltip';
import { usePersonSearch, type SearchField } from '../hooks/usePersonSearch';
import { import {
CheckCircle, CheckCircle,
XCircle, XCircle,
@ -34,7 +35,9 @@ import {
Star, Star,
Info, Info,
Search, Search,
X X,
Database,
Filter as FilterIcon
} from 'lucide-react'; } from 'lucide-react';
// Name similarity calculation using Levenshtein distance // Name similarity calculation using Levenshtein distance
@ -270,8 +273,23 @@ export default function EntityReviewPage() {
type StatsFilter = 'all' | 'reviewed' | 'pending'; type StatsFilter = 'all' | 'reviewed' | 'pending';
const [statsFilter, setStatsFilter] = useState<StatsFilter>('pending'); const [statsFilter, setStatsFilter] = useState<StatsFilter>('pending');
// Profile search // Profile search - now with semantic search mode
const [profileSearchQuery, setProfileSearchQuery] = useState(''); const [profileSearchQuery, setProfileSearchQuery] = useState('');
const [useSemanticSearch, setUseSemanticSearch] = useState(false); // Toggle: semantic vs local filter
// Semantic search hook (searches ALL profiles in vector database)
const {
query: semanticQuery,
setQuery: setSemanticQuery,
searchField: semanticSearchField,
setSearchField: setSemanticSearchField,
results: semanticResults,
isSearching: semanticSearching,
error: semanticError,
queryTimeMs: semanticQueryTime,
resultCount: semanticResultCount,
clearSearch: clearSemanticSearch,
} = usePersonSearch({ debounceMs: 400, minQueryLength: 2, maxResults: 50 });
// Linkup search state // Linkup search state
const [linkupSearching, setLinkupSearching] = useState(false); const [linkupSearching, setLinkupSearching] = useState(false);
@ -857,29 +875,159 @@ export default function EntityReviewPage() {
<div className="review-content"> <div className="review-content">
{/* Profile List Sidebar */} {/* Profile List Sidebar */}
<aside className="profile-sidebar compact"> <aside className="profile-sidebar compact">
<h2>Profielen ({profileSearchQuery ? `${filteredProfiles.length} / ${profiles.length}` : profiles.length})</h2> <h2>Profielen ({useSemanticSearch && semanticQuery ? `${semanticResultCount} found` : profileSearchQuery ? `${filteredProfiles.length} / ${profiles.length}` : profiles.length})</h2>
{/* Profile Search */} {/* Enhanced Profile Search */}
<div className="profile-search"> <div className="profile-search-enhanced">
<Search size={16} /> {/* Search Mode Toggle */}
<input <div className="search-mode-toggle">
type="text"
placeholder={language === 'nl' ? 'Zoek op naam of domein...' : 'Search by name or domain...'}
value={profileSearchQuery}
onChange={(e) => setProfileSearchQuery(e.target.value)}
/>
{profileSearchQuery && (
<button <button
className="clear-search" className={`mode-btn ${!useSemanticSearch ? 'active' : ''}`}
onClick={() => setProfileSearchQuery('')} onClick={() => {
title={language === 'nl' ? 'Wissen' : 'Clear'} setUseSemanticSearch(false);
clearSemanticSearch();
}}
title={language === 'nl' ? 'Filter huidige pagina' : 'Filter current page'}
> >
<X size={14} /> <FilterIcon size={14} />
<span>{language === 'nl' ? 'Pagina' : 'Page'}</span>
</button> </button>
<button
className={`mode-btn ${useSemanticSearch ? 'active' : ''}`}
onClick={() => {
setUseSemanticSearch(true);
setProfileSearchQuery('');
}}
title={language === 'nl' ? 'Zoek alle profielen (vector database)' : 'Search all profiles (vector database)'}
>
<Database size={14} />
<span>{language === 'nl' ? 'Alles' : 'All'}</span>
</button>
</div>
{/* Field Filter (only for semantic search) */}
{useSemanticSearch && (
<div className="search-field-filter">
<select
value={semanticSearchField}
onChange={(e) => setSemanticSearchField(e.target.value as SearchField)}
className="field-select"
>
<option value="all">{language === 'nl' ? 'Alle velden' : 'All fields'}</option>
<option value="name">{language === 'nl' ? 'Naam' : 'Name'}</option>
<option value="email">{language === 'nl' ? 'E-mail' : 'Email'}</option>
<option value="domain">{language === 'nl' ? 'Domein' : 'Domain'}</option>
<option value="birth_year">{language === 'nl' ? 'Geboortejaar' : 'Birth Year'}</option>
</select>
</div>
)}
{/* Search Input */}
<div className="profile-search">
<Search size={16} />
<input
type="text"
placeholder={useSemanticSearch
? (language === 'nl' ? 'Zoek in alle profielen...' : 'Search all profiles...')
: (language === 'nl' ? 'Filter op naam of domein...' : 'Filter by name or domain...')
}
value={useSemanticSearch ? semanticQuery : profileSearchQuery}
onChange={(e) => {
if (useSemanticSearch) {
setSemanticQuery(e.target.value);
} else {
setProfileSearchQuery(e.target.value);
}
}}
/>
{(useSemanticSearch ? semanticQuery : profileSearchQuery) && (
<button
className="clear-search"
onClick={() => {
if (useSemanticSearch) {
clearSemanticSearch();
} else {
setProfileSearchQuery('');
}
}}
title={language === 'nl' ? 'Wissen' : 'Clear'}
>
<X size={14} />
</button>
)}
{semanticSearching && (
<Loader2 className="animate-spin search-spinner" size={14} />
)}
</div>
{/* Search Stats (semantic mode only) */}
{useSemanticSearch && semanticQuery && !semanticSearching && semanticResultCount > 0 && (
<div className="search-stats">
<span>{semanticResultCount} {language === 'nl' ? 'resultaten' : 'results'}</span>
{semanticQueryTime && <span className="query-time">({semanticQueryTime.toFixed(0)}ms)</span>}
</div>
)}
{/* Semantic Search Error */}
{semanticError && (
<div className="search-error">
<AlertCircle size={14} />
<span>{semanticError}</span>
</div>
)} )}
</div> </div>
{loading ? ( {/* Semantic Search Results */}
{useSemanticSearch && semanticQuery && !semanticSearching && semanticResults.length > 0 ? (
<ul className="profile-list semantic-results">
{semanticResults.map((result, idx) => (
<li
key={result.ppid || `semantic-${idx}`}
className="profile-item semantic-result"
onClick={() => {
// If the result has a ppid, try to fetch that profile
if (result.ppid) {
fetchProfileDetail(result.ppid);
}
}}
>
<div className="profile-item-header">
<User size={16} />
<span className="profile-name">{result.name}</span>
{result.score && (
<span className="score-badge" title="Match score">
{(result.score * 100).toFixed(0)}%
</span>
)}
</div>
<div className="profile-item-meta">
{result.headline && (
<span className="headline-text" title={result.headline}>
{result.headline.length > 40 ? result.headline.slice(0, 40) + '...' : result.headline}
</span>
)}
{result.custodian_name && (
<span className="custodian-badge">
<Building2 size={12} />
{result.custodian_name}
</span>
)}
{result.location && (
<span className="location-badge">
<Globe size={12} />
{result.location}
</span>
)}
</div>
</li>
))}
</ul>
) : useSemanticSearch && semanticQuery && !semanticSearching && semanticResults.length === 0 ? (
<div className="empty-state">
<Search size={32} />
<span>{language === 'nl' ? 'Geen resultaten gevonden' : 'No results found'}</span>
</div>
) : loading ? (
<div className="loading-state"> <div className="loading-state">
<Loader2 className="animate-spin" size={24} /> <Loader2 className="animate-spin" size={24} />
<span>{t('loading')}</span> <span>{t('loading')}</span>

View file

@ -1,5 +1,5 @@
{ {
"generated": "2026-01-14T21:38:51.740Z", "generated": "2026-01-14T21:57:09.847Z",
"schemaRoot": "/schemas/20251121/linkml", "schemaRoot": "/schemas/20251121/linkml",
"totalFiles": 3026, "totalFiles": 3026,
"categoryCounts": { "categoryCounts": {

View file

@ -143,7 +143,9 @@ imports:
- ../slots/url - ../slots/url
- ../slots/validation_status - ../slots/validation_status
- ../slots/wikidata - ../slots/wikidata
- ../slots/wikidata_class # REMOVED: ../slots/wikidata_class - migrated to is_or_was_instance_of with WikiDataEntry (2026-01-14, Rule 53)
- ../slots/is_or_was_instance_of
- ./WikiDataEntry
- ../slots/has_or_had_restriction - ../slots/has_or_had_restriction
- ./Restriction - ./Restriction
- ./FindingAid - ./FindingAid
@ -268,7 +270,8 @@ classes:
- url - url
- temporal_extent # was: valid_from + valid_to - migrated per Rule 53 - temporal_extent # was: valid_from + valid_to - migrated per Rule 53
- has_or_had_web_claim - has_or_had_web_claim
- wikidata_class # REMOVED: wikidata_class - migrated to is_or_was_instance_of with WikiDataEntry (2026-01-14, Rule 53)
- is_or_was_instance_of
slot_usage: slot_usage:
id: id:
identifier: true identifier: true

View file

@ -23,7 +23,9 @@ imports:
# REMOVED: ../slots/verified_by - migrated to is_or_was_verified_by with Verifier (2026-01-14, Rule 53) # REMOVED: ../slots/verified_by - migrated to is_or_was_verified_by with Verifier (2026-01-14, Rule 53)
- ../slots/is_or_was_verified_by - ../slots/is_or_was_verified_by
- ./Verifier - ./Verifier
- ../slots/word_count # REMOVED: ../slots/word_count - migrated to has_or_had_quantity with WordCount (2026-01-14, Rule 53)
- ../slots/has_or_had_quantity
- ./WordCount
- ./SpecificityAnnotation - ./SpecificityAnnotation
- ./TemplateSpecificityScores - ./TemplateSpecificityScores
- ../enums/GenerationMethodEnum - ../enums/GenerationMethodEnum
@ -89,7 +91,8 @@ classes:
- verification_date - verification_date
# REMOVED: verified_by - migrated to is_or_was_verified_by with Verifier (2026-01-14, Rule 53) # REMOVED: verified_by - migrated to is_or_was_verified_by with Verifier (2026-01-14, Rule 53)
- is_or_was_verified_by - is_or_was_verified_by
- word_count # REMOVED: word_count - migrated to has_or_had_quantity with WordCount (2026-01-14, Rule 53)
- has_or_had_quantity
slot_usage: slot_usage:
source_video: source_video:
range: string range: string
@ -206,12 +209,26 @@ classes:
examples: examples:
- value: 45.3 - value: 45.3
description: Processed in 45.3 seconds description: Processed in 45.3 seconds
word_count: # DEPRECATED: word_count - migrated to has_or_had_quantity with WordCount (2026-01-14, Rule 53)
range: integer # word_count:
# range: integer
# required: false
# minimum_value: 0
# examples:
# - value: 1523
# description: 1,523 words in transcript
has_or_had_quantity:
range: WordCount
required: false required: false
minimum_value: 0 inlined: true
description: |
Word count in the transcript.
MIGRATED from word_count slot (2026-01-14) per Rule 53.
Uses WordCount class for structured quantity with value.
examples: examples:
- value: 1523 - value: |
value: 1523
description: 1,523 words in transcript description: 1,523 words in transcript
character_count: character_count:
range: integer range: integer

View file

@ -14,8 +14,12 @@ imports:
# REMOVED: ../slots/wikidata_entity_id - migrated to has_or_had_identifier with WikiDataIdentifier (2026-01-14, Rule 53) # REMOVED: ../slots/wikidata_entity_id - migrated to has_or_had_identifier with WikiDataIdentifier (2026-01-14, Rule 53)
- ../slots/has_or_had_identifier - ../slots/has_or_had_identifier
- ./WikiDataIdentifier - ./WikiDataIdentifier
- ../slots/wikidata_entity_label # REMOVED: ../slots/wikidata_entity_label - migrated to has_or_had_label with Label (2026-01-14, Rule 53)
- ../slots/wikidata_mapping_rationale - ../slots/has_or_had_label
- ./Label
# REMOVED: ../slots/wikidata_mapping_rationale - migrated to has_or_had_rationale with Rationale (2026-01-14, Rule 53)
- ../slots/has_or_had_rationale
- ./Rationale
- ../slots/has_or_had_type - ../slots/has_or_had_type
classes: classes:
WikidataAlignment: WikidataAlignment:
@ -26,9 +30,11 @@ classes:
slots: slots:
# REMOVED: wikidata_entity_id - migrated to has_or_had_identifier with WikiDataIdentifier (2026-01-14, Rule 53) # REMOVED: wikidata_entity_id - migrated to has_or_had_identifier with WikiDataIdentifier (2026-01-14, Rule 53)
- has_or_had_identifier - has_or_had_identifier
- wikidata_entity_label # REMOVED: wikidata_entity_label - migrated to has_or_had_label with Label (2026-01-14, Rule 53)
- has_or_had_label
- has_or_had_type - has_or_had_type
- wikidata_mapping_rationale # REMOVED: wikidata_mapping_rationale - migrated to has_or_had_rationale with Rationale (2026-01-14, Rule 53)
- has_or_had_rationale
slot_usage: slot_usage:
has_or_had_identifier: has_or_had_identifier:
range: WikiDataIdentifier range: WikiDataIdentifier
@ -42,6 +48,29 @@ classes:
qid: Q27032435 qid: Q27032435
label: "academic archive" label: "academic archive"
description: Wikidata Q-number with optional label description: Wikidata Q-number with optional label
has_or_had_label:
range: Label
description: |
Human-readable label for the Wikidata entity.
MIGRATED from wikidata_entity_label slot (2026-01-14) per Rule 53.
Uses Label class for structured label with language code.
examples:
- value: |
label_value: "academic archive"
language_code: "en"
description: English label for the Wikidata entity
has_or_had_rationale:
range: Rationale
description: |
Rationale for the mapping between local class and Wikidata entity.
MIGRATED from wikidata_mapping_rationale slot (2026-01-14) per Rule 53.
Uses Rationale class for structured rationale text.
examples:
- value: |
rationale_text: "AcademicArchive is semantically equivalent to Q27032435"
description: Mapping rationale
has_or_had_type: has_or_had_type:
range: MappingType range: MappingType
description: | description: |

File diff suppressed because it is too large Load diff