With Enterprise Application Search dropped from Aurena, I came up with a way to crudely replicate this feature using just straight relational Oracle, with no add-ins.
This scans every leftmost column of every index, using both primary and secondary indexes. It only returns results where the table has a _PK
index associated with it. I excluded ROWKEY
to speed it up a bit, but it’s still somewhat slow.
As well as scanning common indexes, it also scans any UPPER()
function-based indexes you might have. It’s otherwise case-sensitive.
The system load on a scan this big is fairly risky, so it only does an exact match. Users typically don’t understand why HelloWorld%
is so much faster than %HelloWorld
, and I didn’t want to spend time parsing the expression for safety.
I recommend using this for specific searches. Don’t use an internal company ID, for example.
You might implement this with a SQL Quick Report like this:
SELECT *
FROM TABLE(c_enterprise_search_api.get_search_results('&SEARCH_STRING'));
Here’s the code:
CREATE OR REPLACE PACKAGE c_enterprise_search_api IS
/*
module_ CONSTANT VARCHAR2(25) := 'FNDBAS';
lu_name_ CONSTANT VARCHAR2(25) := 'CEnterpriseSearch';
*/
TYPE search_result_typ_ IS TABLE OF VARCHAR2(4000);
FUNCTION get_search_results(
search_string_ IN VARCHAR2) RETURN search_result_typ_ PIPELINED;
FUNCTION get_business_object_from_rowid(
objid_ IN ROWID) RETURN VARCHAR2;
PROCEDURE init;
END c_enterprise_search_api;
/
CREATE OR REPLACE PACKAGE BODY c_enterprise_search_api IS
FUNCTION get_search_results(
search_string_ IN VARCHAR2) RETURN search_result_typ_ PIPELINED
IS
piped_result_ VARCHAR2(32767);
TYPE search_cursor_typ_ IS REF CURSOR;
search_cursor_ search_cursor_typ_;
sql_statement_ VARCHAR2(32767);
search_result_rowid_ VARCHAR2(32767); -- ROWID isn't as safe, since
BEGIN
FOR index_rec_ IN (
SELECT DISTINCT uic.table_name,
'"' || uic.column_name || '"' AS column_expression
FROM user_ind_columns uic
JOIN user_ind_columns uic_pk
ON uic_pk.table_name = uic.table_name
AND uic_pk.index_name LIKE '%\_PK' ESCAPE '\'
AND uic_pk.table_name LIKE '%\_TAB' ESCAPE '\'
AND uic_pk.table_name = REPLACE(uic_pk.index_name, '_PK', '_TAB')
JOIN user_tab_cols utc
ON utc.table_name = uic.table_name
AND utc.column_name = uic.column_name
WHERE uic.column_position = 1
AND utc.data_type = 'VARCHAR2'
AND utc.column_name != 'ROWKEY' -- performance
) LOOP
sql_statement_ :=
'SELECT rowid FROM "' || index_rec_.table_name || '" '
|| 'WHERE ' || index_rec_.column_expression || ' = :search_string';
OPEN search_cursor_ FOR sql_statement_ USING search_string_;
LOOP
FETCH search_cursor_ INTO search_result_rowid_;
EXIT WHEN search_cursor_%NOTFOUND;
piped_result_ :=
'table_name = '
|| index_rec_.table_name
|| ', primary key = '
|| get_business_object_from_rowid(search_result_rowid_);
PIPE ROW (piped_result_);
END LOOP;
END LOOP;
FOR index_rec_ IN (
SELECT table_name,
column_expression
FROM user_ind_expressions
WHERE column_position = 1
) LOOP
-- This code duplication is unavoidable unless you're more clever than me.
-- COLUMN_EXPRESSION is a LONG, so it can't be UNIONED with COLUMN_NAME.
-- COLUMN_EXPRESSION is a LONG, so it can't be filtered in SQL.
-- PIPE can't be called from a child procedure.
IF UPPER(index_rec_.column_expression) LIKE 'UPPER(%' THEN
sql_statement_ :=
'SELECT rowid FROM "' || index_rec_.table_name || '" '
|| 'WHERE ' || index_rec_.column_expression || ' = :search_string';
OPEN search_cursor_ FOR sql_statement_ USING search_string_;
LOOP
FETCH search_cursor_ INTO search_result_rowid_;
EXIT WHEN search_cursor_%NOTFOUND;
piped_result_ :=
'table_name = '
|| index_rec_.table_name
|| ', primary key = '
|| get_business_object_from_rowid(search_result_rowid_);
PIPE ROW (piped_result_);
END LOOP;
END IF;
END LOOP;
END get_search_results;
FUNCTION get_business_object_from_rowid(
objid_ IN ROWID) RETURN VARCHAR2
IS
table_name_ VARCHAR2(30);
view_name_ VARCHAR2(30);
index_name_ VARCHAR2(30);
key_value_ VARCHAR2(32767);
sql_ VARCHAR2(32767);
index_count_ NUMBER;
TYPE cur_typ_ IS REF CURSOR;
cur_ cur_typ_;
TYPE rec_typ_ IS RECORD(
key_value VARCHAR2(4000));
rec_ rec_typ_;
BEGIN
SELECT object_name
INTO table_name_
FROM user_objects
WHERE object_id = dbms_rowid.rowid_object(objid_);
IF table_name_ NOT LIKE '%\_TAB' ESCAPE '\' THEN
RETURN 'ROWID=' || objid_ || '^';
END IF;
view_name_ := REGEXP_REPLACE(table_name_, '_TAB$', '');
index_name_ := view_name_ || '_PK';
SELECT COUNT(*)
INTO index_count_
FROM user_indexes
WHERE index_name = index_name_;
IF index_count_ = 0 THEN
RETURN 'ROWID=' || objid_ || '^';
END IF;
FOR col_rec_ IN (
SELECT ic.column_name,
DECODE(tc.data_type,
'VARCHAR2', '"' || ic.column_name || '"',
'CHAR', '"' || ic.column_name || '"',
'NUMBER', 'TO_CHAR("' || ic.column_name || '")',
'DATE', 'TO_CHAR("' || ic.column_name || '", ''YYYY-MM-DD HH24:MI:SS'')',
'RAW', 'RAWTOHEX("' || ic.column_name || '")',
'''BAD COLUMN''') AS expression
FROM user_ind_columns ic
INNER JOIN user_tab_cols tc
ON tc.table_name = ic.table_name
AND tc.column_name = ic.column_name
WHERE ic.index_name = index_name_
)
LOOP
sql_ :=
'SELECT ' || col_rec_.expression || ' AS key_value'
|| ' FROM ' || table_name_
|| ' WHERE rowid = :objid_';
BEGIN
OPEN cur_ FOR sql_ USING IN objid_;
FETCH cur_ INTO rec_;
CLOSE cur_;
EXCEPTION
WHEN OTHERS THEN
CLOSE cur_;
RAISE;
END;
key_value_ := key_value_
|| col_rec_.column_name || '=' || rec_.key_value || '^';
END LOOP;
RETURN key_value_;
END get_business_object_from_rowid;
PROCEDURE init IS BEGIN NULL; END init;
END c_enterprise_search_api;
/