cad.net 倒序索引

概念性文章,不做任何运行保证,只做原理设计

查找字符串

构造倒序索引解决查找替换字符串速度慢问题,
它是单线程方案,只是查询不同doc的key可以并行多线程.
例如如何从十万个dwg里面找到"2004年建筑规范",
需要构造map来储存文件路径,文字所属的句柄,
同时最好了解一些Everything原理来避免遍历磁盘文件.

1,打开图纸后遍历全部文本,
通过分词器分词,写入字典,
key是词,value是文字的id集合(一词多行).

2,通过不同的事件进行维护索引,
事件有两种,一种是图元事件,一种是数据库事件,
用后者比较简单,不需要更改每个图元.
数据库加入事件/数据库移除事件/数据库更改事件.
撤回和重做本质只是删除和更改!!也会触发对应的事件的!!

3,高频替换时候,就可以通过这个字典进行了.
通过id找词叫正序索引,而反之就是倒序索引.
把找到的多个ids合并,就是命中的语句们了.

这就是人们常说的用空间换时间的方式.也是搜索引擎的原理.
基本上全部CAD二次索引都是这样做.
你必须要用单线程方案,
否则并行遍历块表无法重置迭代器(除非找到更改指针方案).
你会发现桌子就是少做很多索引组织表,
然后导致你需要各种遍历.

原理

原理的视频:
ES数据库: https://b23.tv/2Jwi1gG

.NET分词器很多的,只需要选择其中一款就好了,并且需要支持中文.
https://www.cnblogs.com/linezero/p/jiebanetcore.html

Encoding.RegisterProvider(CodePagesEncodingProvider.Instance);
var segmenter = new JiebaSegmenter();
var segments = segmenter.Cut("我来到广州华南理工大学", cutAll: true);
Console.WriteLine("【全模式】:{0}", string.Join("/ ", segments));

CAD事件,这个网站好看点:
http://mac.bb-mac.com/help/books/.net/AutoCAD.net/files/WS1a9193826455f5ff-e569a0121d1945c08-2024.htm

代码

此代码没有经过本人测试,不知道事件顺序,
所以可能根本跑不起来,只做原理说明而已

public class SddSetCommands {
    HashSet<SingleDatabaseDictionary> SddSet = new();

    [IFoxInitialize]
    public void Initialize() {
        // 启动的两种模式都要做:
        // 1,通过注册表启动,必须用文档管理器加载文档事件,
        // 等待界面完成,直到文档出现后触发事件,然后扫掠全部文档.
        // 2,通过netload加载启动,需要尝试直接加入.
        var dm = Acap.DocumentManager;
        if (dm is null) return;
        if (dm.Count != 0) {
            foreach (Document doc in dm) {
                HashAdd(doc);
            }
        }
        // dm的是全局事件可以不卸载,doc和db事件则需要注意卸载
        dm.DocumentCreated += DmCreated;
        dm.DocumentToBeDestroyed -= DmDestroyed;
    }

    // 打开文档,如果数据库已经存盘就会加入,否则跳过
    void DmCreated(object sender, DocumentCollectionEventArgs e) {
        Env.Printl("DmCreated");
        HashAdd((Document)sender);
    }

    // 文档关闭就释放对应的字典
    void DmDestroyed(object sender, DocumentCollectionEventArgs e) {
        Env.Printl("DmDestroyed");
        var doc = (Document)sender;
        LoadCommandEnded(doc, false);
        SddSet.Remove(new(doc.Database));
    }

    void HashAdd(Document doc) {
        if (doc is null)
            throw new ArgumentNullException("doc is null");
        if (doc.IsReadOnly) return;

        // 未保存不加入
        var file = Path.Combine(Path.GetDirectoryName(doc.Database.OriginalFileName), doc.Name);
        var originEx = Path.GetExtension(file).ToLower();
        if (originEx != ".dwg" || originEx != ".dxf") return;

        var dict = new SingleDatabaseDictionary(doc.Database);
        if (!SddSet.Contains(dict)) {
            SddSet.Add(dict);
            dict.Builder();
        }
        LoadCommandEnded(doc);
    }

    // todo 命令事件改为保存事件
    bool LoadCommandEnded(Document doc, bool isload = true) {
       if(isload) doc.CommandEnded += DocCommandEnded;
       else doc.CommandEnded -= DocCommandEnded;
    }

    /// <summary>
    /// 命令完成后(内锁文档)
    /// </summary>
    void DocCommandEnded(object sender, CommandEventArgs e) {
        // 过滤噪声
        if (string.IsNullOrEmpty(e.GlobalCommandName)
           || e.GlobalCommandName == "#")
            return;

        Env.Printl("DocCommandEnded");
        Env.Printl(e.GlobalCommandName.ToUpper());

        var doc = (Document)sender;
        switch (e.GlobalCommandName.ToUpper()) {
            case "SAVEAS":
                // var num = Acap.GetSystemVariable("DBMode");
                // if (num == ?)
                // 保存数据库,就加入分析.
                // 如果是关闭时候保存,就不加入啊.(不知道如何实现)
                // 反正重复加入不进去HashSet
                HashAdd(doc);
                break;
        }
    }

    // 查询
    public void Find(string txt, Action<Database, HashSet<ObjectId>> action) {
        var segments = SingleDatabaseDictionary.Segmenter.Cut(txt, cutAll: true); // 分词
        Env.Printl("查询器分词:" + string.Join(" ", segments));

        // 遍历每个数据库的字典(可以改为并行)
        foreach (var dict in SddSet) {
            // 命中词汇的行(文本ObjectId)
            HashSet<ObjectId> set = new();
            // 遍历每个词,多个词可能在同一行,通过hashset过滤
            // 搜索引擎还可以根据出现数量来作为关联度,以此置顶.
            foreach (var se in segments) {
                if (dict.Words.TryGetValue(se, out var ids))
                    set.UnionWith(ids);
            }
            // Env.Printl("数据库:{dict.DwgFile}");
            // Env.Printl("id是:{string.Join(" ", set)}");
            action.Invoke(dict.Database, set); // 数据库用于事务,set用来修改
        }
    }
}


public class SingleDatabaseDictionary {

    // 公开字段用于序列化
    public static JiebaSegmenter Segmenter = new(); // 分词器
    public Database Database;
    public string DwgFile => Database.Filename; // 不缓存,保存之后会变
    public Dictionary<string, HashSet<ObjectId>> Words; // 倒序索引
    public override int GetHashCode() {
        return DwgFile.GetHashCode();
    }
    public override bool Equals(object obj) {
        if (obj is SingleDatabaseDictionary other)
            return DwgFile == other.DwgFile;
        return false;
    }
    public SingleDatabaseDictionary(Database db) {
        if (db is null)
            throw new ArgumentNullException("db is null");
        Database = db;
        Words = new();
    }
    // 直接打开图纸后遍历构建倒序索引
    public void Builder() {
        // 通过数据库事件,就不用附加事件到图元了
        Database.ObjectAppended += DbAppended; // todo没有注销
        Database.ObjectErased += DbErased; // 撤回时候删除
        Database.ObjectModified += DBModified; // 撤回时候更改
        Database.ObjectUnappended += DbUnappended;
        Database.ObjectReappended += DbReappended;

        using DBTrans tr = new(Database);
        foreach (var bid in tr.BlockTable) {
            if (!bid.IsOk()) continue;
            using var btr = tr.GetObject<BlockTableRecord>(bid, OpenMode.ForRead);
            foreach (var eid in btr) {
                if (!eid.IsOk()) continue;
                using var ent = tr.GetObject<Entity>(eid, OpenMode.ForRead);
                if (ent is DBText dbtext) {
                    // ent.Modified += EntModified; // todo没有注销
                    // 和下面冲突 ent.ModifyUndone += EntOpenedForModify;
                    // ent.OpenedForModify += EntOpenedForModify;
                    AddNewly(dbtext.ObjectId, dbtext.TextString);
                }
                else if (ent is MText mtext) {
                    // ent.Modified += EntModified;
                    // 和下面冲突 ent.ModifyUndone += EntOpenedForModify;
                    // ent.OpenedForModify += EntOpenedForModify;
                    AddNewly(mtext.ObjectId, mtext.Text);
                }
            }
        }
    }

    // 数据库内容新增
    private void DbAppended(object sender, ObjectEventArgs e) {
        Env.Printl("DbAppended");
        if (e.DBObject is DBText dbtext) {
            AddNewly(dbtext.ObjectId, dbtext.TextString);
        }
        else if (e.DBObject is MText mtext) {
            AddNewly(mtext.ObjectId, mtext.Text);
        }
    }

    /// <summary>
    /// 撤回事件(获取删除对象)
    /// 它会获取有修改步骤的图元id
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void DbErased(object sender, ObjectErasedEventArgs e) {
        Env.Printl("DbErased");
        // if (!State.IsRun) return;
        if (e.Erased) return; // 跳过,否则无法读取图元信息

        if (e.DBObject is DBText dbtext) {
            RemoveOld(dbtext.ObjectId, dbtext.TextString);
        }
        else if (e.DBObject is MText mtext) {
            RemoveOld(mtext.ObjectId, mtext.Text);
        }
    }

    /// <summary>
    /// 撤回事件(更改时触发)
    /// 它会获取有修改步骤的图元id
    /// </summary>
    /// <param name="sender"></param>
    /// <param name="e"></param>
    private void DBModified(object sender, ObjectEventArgs e) {
        Env.Printl("DBModified");
        // if (!State.IsRun) return;
        if (!e.DBObject.IsUndoing || e.DBObject.IsErased) return;
        DbAppended(sender, e);
    }

    // 撤回事件,拉伸填充没有此事件,可能不需要.
    // 只需要上面两个
    private void DbUnappended(object sender, ObjectEventArgs e) {
        Env.Printl("DbUnappended");
        // DbErased(sender, e);
    }
    // 撤回后重做,拉伸填充没有此事件,可能不需要.
    // 重做不过也是删除和修改,可能不需要
    private void DbReappended(object sender, ObjectEventArgs e) {
        Env.Printl("DbReappended");
        // DbAppended(sender, e);
    }

    // 图元修改前,用数据库事件代替
    //private void EntOpenedForModify(object sender, EventArgs e) {
    //    DbErased(sender, e);
    //}
    //// 图元修改后,用数据库事件代替
    //private void EntModified(object sender, EventArgs e) {
    //    DbAppended(sender, e);
    //}

    private void RemoveOld(ObjectId id, string txt) {
        var segments = Segmenter.Cut(txt, cutAll: true);
        foreach (var item in segments) {
            if (Words.ContainsKey(item)) {
                Words[item].Remove(id);
                if (Words[item].Count == 0)
                    Words.Remove(item);
            }
        }
    }

    private void AddNewly(ObjectId id, string txt) {
        var segments = Segmenter.Cut(txt, cutAll: true);
        foreach (var item in segments) {
            if (!Words.ContainsKey(item))
                Words.Add(item, new HashSet<ObjectId> { id });
            else
                Words[item].Add(id);
        }
    }
}

嵌套组处理

因为深度克隆带组的图元,手选克隆后其中一个新图元,
会和旧组一起选择,但是新图元并没有加入组,
怀疑是图元上面记录了组,而不是组记录了对象.
解决方案一:移除图元内记录.
解决方案二:克隆前记录原本的组的成员,解组,再克隆,最后创建组还原.
解决方案三:浅克隆不会有这些问题.

浅克隆

using DBTrans tr = new();
var getRes = Env.Editor.GetSelection();
if (getRes.Status != PromptStatus.OK) return;

// [旧组名,旧组内图元]
var gs = new Dictionary<string, ObjectId[]>();
// [旧图元,新图元]
var idMap = new Dictionary<ObjectId, ObjectId>();

var getEnts = getRes.Value.GetEntities<Entity>();
foreach (var ent in getEnts) {
    idMap.Add(ent.Id, tr.CurrentSpace.AddEntity(ent.CloneEx()));
    // 遍历图元的组,构造映射关系
    foreach (var group in ent.GetGroups()) {
        if (gs.ContainsKey(group.Name)) continue;
        gs.Add(group.Name, group.GetAllEntityIds());
    }
}

// 每次迭代同一个组的,然后通过字典获取新的,即为新组图元.
foreach (var ids in gs.Values)
    tr.GroupDict.AddGroup("*U", ids.Select(id => idMap[id]));

组数量排序

public class GroupInfo : IEquatable<GroupInfo> {
    public int Count {get; private set;}
    public ObjectId ObjectId {get;}
    public string Name {get;}
    public GroupInfo(string name, ObjectId id, int count) {
        Count = count;
        ObjectId = id;
        Name = name;
    }

    public void SetCount(int count){
        Count = count;
    }

    // 利用它排序
    public override int GetHashCode() {
        return Count;
    }

    // 为了方案一,所以只比较id
    public bool Equals(GroupInfo? other) {
        if (other is null) return false;
        return this.ObjectId.Equals(other.ObjectId);
    }

    public bool EqualsAll(GroupInfo? other) {
        if (other is null) return false;
        return this.ObjectId.Equals(other.ObjectId)
            && this.Count == other.Count
            && this.Name == other.Name;
    }

}


public class GroupDictionary {
// 因为组是可以嵌套的,
// 嵌套的方式就是少到多,因此排序就找到最下层的.
// 倒序索引
// [图元id,组ids]
public Dictionary<ObjectId, SortedSet<GroupInfo>> IdMap = new();

public GroupDictionary(Database? db = null) {
    using DBTrans tr = new(db);
    using var groups = (DBDictionary)tr.GetObject(
        db.GroupDictionaryId,OpenMode.ForRead);

    // 遍历所有的组
    foreach (DBDictionaryEntry entry in groups) {
        using var group = (Group)tr.GetObject(entry.Value, OpenMode.ForRead);
        var eids = group.GetAllEntityIds();

        // 遍历组下的图元
        foreach (var eid in eids) {
            //using var ent = (Entity)tr.GetObject(eid, OpenMode.ForRead);
            var gi = new GroupInfo(entry.Key, group.ObjectId, eids.Length);
            if(!IdMap.ContainsKey(eid)){
                IdMap.Add(eid, new(){gi});
            }
            else {
                IdMap[eid].Add(gi);
            }
       }

    }
}
}

命令

[CommandMethod(nameof(DeepCloneGroupCmd))]
public void DeepCloneGroupCmd() {
    using DBTrans tr = new();
    var filter = new SelectionFilter(new TypedValue[0]);
    var ss = Env.Editor.GetSelection(filter);
    if (ss.Status != PromptStatus.OK)
        return;
    ObjectId[] ids = ss.Value.GetObjectIds();
    for (int i = 0; i < ids.Length; i++) {
        using var ent = tr.GetObject<Entity>(ids[i], OpenMode.ForRead);
        DeepCloneGroup(ent);
    }
}

深克隆排序方案一

public void DeepCloneGroup(Entity sent) {
var tr = DBTrans.Top;

var IdMapper = sent.DeepCloneEx();
var ids = IdMapper.Keys; //克隆后新id....还是Values?

// [旧组,要创建同组的图元们] 此数据结构会排序
SortedDictionary<GroupInfo, List<ObjectId>> smap = new();

// 获取图元加入了哪个旧组,并移除.
foreach (var id in ids) {
    using var ent = tr.GetObject(id, OpenMode.ForRead);
    // 一个图元多个组
    var gids = ent.GetGroups();
    foreach (var gid in gids) {
        using var group = (Group)tr.GetObject(gid, OpenMode.ForRead);
        var length = group.GetAllEntityIds().Length;
        group.UpgradeOpen();
        group.Remove(id);
        group.DowngradeOpen();
        ent.RemoveGroup(group.ObjectId);
        // 记录旧组信息
        var gi = new GroupInfo(group.Name, group.ObjectId, length-1);
        List<ObjectId> idList = new();

        // ContainsKey调用GroupInfo.Equals,
        // 因此Equals改成只比较组id.
        // 若不改Equals可以用方案二倒腾结构,不过速度慢了.
        // 先移除再加入才能根据length作为hash重排序
        if (smap.ContainsKey(gi)) {
            idList = samp[gi];
            smap.Remove(gi);
        }
        idList.Add(id);
        smap.Add(gi, idList);
    }
}

// 根据排序创建新组,升序,少在前多在后
// 筛选出同组的新ids,根据不同组内数量,循环构建.
foreach(var item in smap) {
    tr.GroupDict.AddGroup("*U", item.Pair.Value);
}
}

深克隆排序方案二

GroupInfo类Equals是全部字段比较的,
不需要改为只比较组id,当然,只比较也是可以的.

public void DeepCloneGroup(Entity sent) {
var tr = DBTrans.Top;
// var gd = new GroupDictionary(tr.Database);

// 深度克隆后的图元会自动加入组中
var IdMapper = ent.DeepCloneEx();
ids = IdMapper.Keys; //克隆后新id....还是Values?

// [图元,所属旧组们]
Dictionary<ObjectId, HashSet<GroupInfo>> map = new();

// 获取图元加入了哪个旧组,并移除.
foreach (var id in ids) {
    using var ent = tr.GetObject(id, OpenMode.ForRead);
    // 一个图元多个组
    var gids = ent.GetAllGroupObjectIds();
    foreach (var gid in gids) {
        using var group = (Group)tr.GetObject(gid, OpenMode.ForRead);
        group.UpgradeOpen();
        group.Remove(id);
        group.DowngradeOpen();
        ent.RemoveGroup(group.ObjectId);
        // 记录旧组信息
        // ent不会重复记录组,所以这里其实不需要hashset<gi>
        var gi = new GroupInfo(group.Name, group.ObjectId, -1);
        if (!map.ContainsKey(id))
            map.Add(id, new(){ gi });
        else
            map[id].Add(gi);
    }
}

// 遍历组,设定组信息图元数量用于排序
foreach(var item in map) {
    foreach(var gi in item.Pair.Value) {
        if (gi.Count != -1) continue; // 跳过已经赋值的
        using var group = (Group)tr.GetObject(gi.ObjectId, OpenMode.ForRead);
        gi.SetCount(group.GetAllEntityIds().Length);
    }
}

// [旧组,要创建同组的图元们] 此数据结构会排序
SortedDictionary<GroupInfo, List<ObjectId>> smap = new();
foreach (var item in map) {
     foreach(var gi in item.Pair.Value) {
        if (!smap.ContainsKey(gi))
            smap.Add(gi, new(){ item.Key });
        else
            smap[gi].Add(item.Key);
    }
}

// 根据排序创建新组,升序,少在前多在后
// 筛选出同组的新ids,根据不同组内数量,循环构建.
foreach(var item in smap) {
    tr.GroupDict.AddGroup("*U", item.Pair.Value);
}
}

(完)

posted @ 2024-12-01 18:55  惊惊  阅读(84)  评论(0编辑  收藏  举报